Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you share data between view controllers and other objects in Swift?

Tags:

ios

swift

Say I have multiple view controllers in my Swift app and I want to be able to pass data between them. If I'm several levels down in a view controller stack, how do I pass data to another view controller? Or between tabs in a tab bar view controller?

(Note, this question is a "ringer".) It gets asked so much that I decided to write a tutorial on the subject. See my answer below.

like image 439
Duncan C Avatar asked Apr 19 '15 19:04

Duncan C


People also ask

How do I navigate between view controllers in Swift?

Control-drag from the button in the first view controller into the second view controller. In the dialog that pops up, select show. A top bar should appear in the second view controller and there should be a connection between our first view controller to our second view controller.


2 Answers

Your question is very broad. To suggest there is one simple catch-all solution to every scenario is a little naïve. So, let's go through some of these scenarios.


The most common scenario asked about on Stack Overflow in my experience is the simple passing information from one view controller to the next.

If we're using storyboard, our first view controller can override prepareForSegue, which is exactly what it's there for. A UIStoryboardSegue object is passed in when this method is called, and it contains a reference to our destination view controller. Here, we can set the values we want to pass.

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

Alternatively, if we're not using storyboards, then we're loading our view controller from a nib. Our code is slightly simpler then.

func showNextController() {     let destination = SecondController(nibName: "SecondController", bundle: nil)     destination.myInformation = self.myInformation     show(destination, sender: self) } 

In both cases, myInformation is a property on each view controller holding whatever data needs to be passed from one view controller to the next. They obviously don't have to have the same name on each controller.


We might also want to share information between tabs in a UITabBarController.

In this case, it's actually potentially even simpler.

First, let's create a subclass of UITabBarController, and give it properties for whatever information we want to share between the various tabs:

class MyCustomTabController: UITabBarController {     var myInformation: [String: AnyObject]? } 

Now, if we're building our app from the storyboard, we simply change our tab bar controller's class from the default UITabBarController to MyCustomTabController. If we're not using a storyboard, we simply instantiate an instance of this custom class rather than the default UITabBarController class and add our view controller to this.

Now, all of our view controllers within the tab bar controller can access this property as such:

if let tbc = self.tabBarController as? MyCustomTabController {     // do something with tbc.myInformation } 

And by subclassing UINavigationController in the same way, we can take the same approach to share data across an entire navigation stack:

if let nc = self.navigationController as? MyCustomNavController {     // do something with nc.myInformation } 

There are several other scenarios. By no means does this answer cover all of them.

like image 90
nhgrif Avatar answered Oct 13 '22 04:10

nhgrif


This question comes up all the time.

One suggestion is to create a data container singleton: An object that gets created once and only once in the life of your application, and persists for the life of your app.

This approach is well suited for a situation when you have global app data that needs to be available/modifiable across different classes in your app.

Other approaches like setting up one-way or 2-way links between view controllers are better suited to situations where you are passing information/messages directly between view controllers.

(See nhgrif's answer, below, for other alternatives.)

With a data container singleton, you add a property to your class that stores a reference to your singleton, and then use that property any time you need access.

You can set up your singleton so that it saves it's contents to disk so that your app state persists between launches.

I created a demo project on GitHub demonstrating how you can do this. Here is the link:

SwiftDataContainerSingleton project on GitHub Here is the README from that project:

SwiftDataContainerSingleton

A demonstration of using a data container singleton to save application state and share it between objects.

The DataContainerSingleton class is the actual singleton.

It uses a static constant sharedDataContainer to save a reference to the singleton.

To access the singleton, use the syntax

DataContainerSingleton.sharedDataContainer 

The sample project defines 3 properties in the data container:

  var someString: String?   var someOtherString: String?   var someInt: Int? 

To load the someInt property from the data container, you'd use code like this:

let theInt = DataContainerSingleton.sharedDataContainer.someInt 

To save a value to someInt, you'd use the syntax:

DataContainerSingleton.sharedDataContainer.someInt = 3 

The DataContainerSingleton's init method adds an observer for the UIApplicationDidEnterBackgroundNotification. That code looks like this:

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() } 

In the observer code it saves the data container's properties to NSUserDefaults. You can also use NSCoding, Core Data, or various other methods for saving state data.

The DataContainerSingleton's init method also tries to load saved values for it's properties.

That portion of the init method looks like this:

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

The keys for loading and saving values into NSUserDefaults are stored as string constants that are part of a struct DefaultsKeys, defined like this:

struct DefaultsKeys {   static let someString  = "someString"   static let someOtherString  = "someOtherString"   static let someInt  = "someInt" } 

You reference one of these constants like this:

DefaultsKeys.someInt 

Using the data container singleton:

This sample application makes trival use of the data container singleton.

There are two view controllers. The first is a custom subclass of UIViewController ViewController, and the second one is a custom subclass of UIViewController SecondVC.

Both view controllers have a text field on them, and both load a value from the data container singlelton's someInt property into the text field in their viewWillAppear method, and both save the current value from the text field back into the `someInt' of the data container.

The code to load the value into the text field is in the viewWillAppear: method:

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)" } 

The code to save the user-edited value back to the data container is in the view controllers' textFieldShouldEndEditing methods:

 func textFieldShouldEndEditing(textField: UITextField) -> Bool  {    //Save the changed value back to our data container singleton    DataContainerSingleton.sharedDataContainer.someInt = textField.text!.toInt()    return true  } 

You should load values into your user interface in viewWillAppear rather than viewDidLoad so that your UI updates each time the view controller is displayed.

like image 34
Duncan C Avatar answered Oct 13 '22 06:10

Duncan C