Back to blog

How To: Pass Data Between View Controllers in Swift


Abhinav Girdhar
By Abhinav Girdhar | October 19, 2023 8:50 am

If your app has multiple User Interfaces (UIs), you’ll want to move data from one UI to the next. How do you pass data between view controllers in Swift?

Passing data between view controllers is an important part of iOS development. You can use several ways to do so, and all of them have distinct advantages and drawbacks.

The ability to pass data between view controllers with ease is affected by your choice of app architecture. App architecture affects how you work with view controllers, and vice versa.

In this app development tutorial, you’ll learn 6 different methods of passing data between view controllers, including working with properties, segues and NSNotificationCenter. You’ll start with the easiest approach, then move on to more complicated practices.

Describe your app idea
and AI will build your App

Passing Data Between View Controllers with Properties (A → B)

You can pass data between view controllers in Swift in 6 ways:

  1. By using an instance property (A → B)
  2. By using segues with Storyboards
  3. By using instance properties and functions (A ← B)
  4. By using the delegation pattern
  5. By using a closure or completion handler
  6. By using NotificationCenter and the Observer pattern

As you’ll soon learn, some of these approaches are one-way, so you can send some data one way, but not the other way around. They’re not bilateral, so to speak. But don’t worry – in most cases you only need one-way communication anyway!

The easiest way to get data from view controller A to view controller B (forward) is by using a property.

A property is a variable that’s part of a class. Every instance of that class will have that property, and you can assign a value to it. View controllers of type UIViewController can have properties just like any other class.

Here’s a view controller MainViewController with a property called text:

class TertiaryViewController: UIViewController
{
var username:String = “”

@IBOutlet weak var usernameLabel:UILabel?

override func viewDidLoad()
{
super.viewDidLoad()

usernameLabel?.text = username
}
}

It’s nothing special – much like the previous example, you’re setting a simple label usernameLabel with a text from property username.

Properties are one of the most straightforward ways to pass data between view controllers. They are especially useful when the data transfer is unidirectional, from a source view controller to a destination view controller. When using properties for data transfer:

  1. Initialization: Ensure that the destination view controller’s properties are initialized before the transition. This can be done in the prepare(for:sender:) method of the source view controller.
  2. Safety: Always check for nil values when accessing properties in the destination view controller. Using optional binding (if let or guard let) can help safely unwrap these values.
  3. Data Types: Properties can be of any data type, including custom objects. Ensure that the data type of the property matches the type of data you want to pass.
  4. Performance: Passing data using properties is efficient, as it doesn’t involve any overhead like serialization or database operations. However, be cautious when passing large data sets or objects, as it might impact memory usage.

Then, to pass the data from MainViewController to TertiaryViewController you use a special function called prepare(for:sender:). This method is invoked before the segue, so you can customize it.

Here’s the segue code in action:

override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
if segue.destination is TertiaryViewController {
let vc = segue.destination as? TertiaryViewController
vc?.username = “Arthur Dent”
}
}

This is what happens:

  • First, with the if statement and the is keyword you check whether the segue destination is of class TertiaryViewController. You need to identify if this is the segue you want to customize, because all segues go through the prepare(for:sender:) function.
  • Then, you simply type cast segue.destination to TertiaryViewController, so you can use the username property. The destination property on segue has type UIViewController, so you’ll need to cast it to get to the username property.
  • Finally, you set the username property, just like you did in the previous example.

The funny thing about the prepare(for:sender:) function is that you don’t have to do anything else. The function simply hooks into the segue, but you don’t have to tell it to continue with the transition. You also don’t have to return the view controller you customized.

You can also improve the above code sample like this:

if let vc = segue.destination as? TertiaryViewController {
vc.username = “Ford Prefect”
}

Instead of using the is keyword to check the type of destination, and then casting it, you now do that in one go with optional casting. When segue.destination isn’t of type TertiaryViewController, the as? expression returns nil and therefore the conditional doesn’t execute. Easy-peasy!

If you don’t want to use type casting, you can also use the segue.identifier property. Set it to tertiaryVC in the Storyboard, and then use this:

if segue.identifier == “tertiaryVC” {
// Do stuff…
}

So… that’s all there is to passing data between view controllers using segues and Storyboards!

For many apps Storyboards limit the different transitions between view controllers you can use. Storyboards often overcomplicate building user interfaces in Xcode, at very little benefit. On top of that, Interface Builder gets slow and laggy if you have complicated Storyboards or XIBs.

Everything you can do with Storyboards you can code by hand, with much greater control and little extra developer effort. I’m not saying you should code your user interfaces by hand, though! Use one XIB per view controller, much like the example above, and subclass views like UITableViewCell.

Ultimately, as a coder, you’ll want to figure out on your own what you like best – tabs or spaces, Storyboards or XIBs, Core Data or Realm – it’s up to you!

Fun Fact: Every developer has their own way of saying the word “segue”. Some pronounce se- as “say” or “set”, and -gue as “gue_rilla”, other simply pronounce it as _seg-way (like the Segway, the flopped two-wheeled self-balancing personal transporter for tourists).

Passing Data Back with Properties and Functions (A ← B)

Now… what if you want to pass data back from a secondary view controller to the main view controller?

Passing data between view controllers using a property on the secondary view controller, as explained in the first chapter, is fairly straightforward. How do you pass data back from the second view controller to the first? You can do this in a couple of ways, as you’ll find out in the next sections.
Here’s the scenario:

  • The user of your app has gone from view controller A to a secondary view controller B.
  • In the secondary view controller the user interacts with a piece of data, and you want that data back in view controller A.

In other words: instead of passing data from A → B, you want to pass data back from B → A.

The easiest way to pass data back is to create a reference to view controller A on view controller B, and then call a function from view controller A within view controller B.
This is now the secondary view controller class:

class SecondaryViewController: UIViewController
{
var mainViewController:MainViewController?

@IBAction func onButtonTap()
{
mainViewController?.onUserAction(data: “The quick brown fox jumps over the lazy dog”)
}
}
Then, this function is added to MainViewController:

func onUserAction(data: String)
{
print(“Data received: \(data)”)
}
When the view controller is pushed onto the navigation stack, just like in the previous examples, a connection between the main view controller and the secondary view controller is made:

let vc = SecondaryViewController(nibName: “SecondaryViewController”, bundle: nil)
vc.mainViewController = self
In the example above, self is assigned to property mainViewController. The secondary view controller now “knows” the main view controller, so it can call any of its functions – like onUserAction(data:).

Passing data back using properties and functions is a direct approach, but it’s essential to be aware of its implications:

  1. Coupling: This method introduces tight coupling between the two view controllers. While it’s straightforward, it can make the code less modular and harder to maintain in larger projects.
  2. Memory Management: Be cautious of retain cycles when using this approach. If both view controllers have strong references to each other, they might not be deallocated, leading to memory leaks. Using weak references can help mitigate this.
  3. Data Integrity: Ensure that the data passed back is validated and sanitized, especially if it’s used for critical operations or displayed to the user.
  4. Function Calls: When using functions to pass data back, ensure that the function in the main view controller is prepared to handle multiple calls or unexpected data, as the secondary view controller might trigger it under various scenarios.

That’s all there is to it. But… this approach for passing data isn’t the most ideal. It has a few major drawbacks:

  • The MainViewController and SecondaryViewController are now tightly coupled. You want to avoid tight-coupling in software design, mostly because it decreases the modularity of your code. Both classes become too entangled, and rely on each other to function properly, with often leads to spaghetti code.
  • The above code example creates a retain cycle. The secondary view controller can’t be removed from memory until the main view controller is removed, but the main view controller can’t be removed from memory until the secondary view controller is removed. (A solution would be the weak property keyword.)
  • Two developers can’t easily work separately on MainViewController and SecondaryViewController, because both view controllers need to have an understanding about how the other view controller works. There’s no separation of concerns.

You want to avoid directly referencing classes, instances and functions like this. Code like this simply becomes a nightmare to maintain. It often leads to spaghetti code, in which you change one piece of code that breaks another seemingly unrelated piece of code…

So, what’s a better idea? Delegation!

Quick Tip: If you want to pass a few variables that belong together between view controllers, don’t create multiple properties. Instead, create a struct or class (a so-called model) that wraps all data, and pass along an instance of that class in one property.

Passing Data Back with Delegation

Delegation is an important and frequently used software design pattern in the iOS SDK. It’s critical to understand if you’re coding iOS apps!

With delegation, a base class can hand off functionality to a secondary class. A coder can then implement this secondary class and respond to events from the base class, by making use of a protocol. It’s decoupled!

Here’s a quick example:

  • Imagine you’re working in a pizza restaurant. You have a pizza baker that makes pizza.
  • Customers can do anything they want with the pizza, like eat it, put it in the freezer, or share it with a friend.
  • The pizza baker used to care about what you do with your pizza, but now he’s decoupled himself from the pizza-eating process and just throws you a pizza when it’s ready. He delegates the pizza handling process and only concerns himself with baking pizzas!

Before you and the pizza baker can understand each other, you need to define a protocol:

protocol PizzaDelegate {
func onPizzaReady(type: String)
}
A protocol is an agreement about what functions a class should implement, if it wants to conform to the protocol. You can add it to a class like this:

class MainViewController: UIViewController, PizzaDelegate
{

This class definition now says:

  • Name the class MainViewController
  • Extend (or subclass) the UIViewController class
  • Implement (or conform to) the PizzaDelegate class

If you say you want to conform to a protocol, you also have to implement it. You add this function to MainViewController:

func onPizzaReady(type: String)
{
print(“Pizza ready. The best pizza of all pizzas is… \(type)”)
}
When you create the secondary view controller, you also create the delegate connection, much like the property in the previous example:

vc.delegate = self
Then, here’s the key aspect of delegation. You now add a property and some code to the class that should delegate functionality, like the secondary view controller.

First, the property:

weak var delegate:PizzaDelegate?
Then, the code:

@IBAction func onButtonTap()
{
delegate?.onPizzaReady(type: “Pizza di Mama”)
}
Let’s say that the function onButtonTap() is called when the pizza baker finishes making a pizza. It then calls onPizzaReady(type:) on delegate.

The pizza baker doesn’t care if there’s a delegate or not. If there’s no delegate, the pizza just ends up thrown away. If there’s a delegate, the pizza baker only hands-off the pizza – you can do with it what you want!

So, let’s take a look at the key components from delegation:

  1. You need a delegate protocol
  2. You need a delegate property
  3. The class that you want to off-hand data to, needs to conform to the protocol
  4. The class that you want to delegate from, needs to call the function defined in the protocol

How is this different from the previous example with passing data back via properties?

  • The developers that work on separate classes only need to agree on the protocol and the functions it has. Either developer can choose to conform and implement whatever they want.
  • There’s no direct connection between the main view controller and the secondary view controller, which means that they’re more loosely coupled than the previous example.
  • The protocol can be implemented by any class, not just the MainViewController.

Awesome! Now let’s look at another example… using closures.

Why is that delegate property marked with weak? Find out more here: Automatic Reference Counting (ARC) in Swift

Passing Data Back with a Closure

Using a closure to pass data between view controllers isn’t much different from using a property or delegation.

The biggest benefit of using a closure is that it’s relatively easy to use, and you can define it locally – no need for a function or protocol.
You start with creating a property on the secondary view controller, like this:

var completionHandler: ((String) -> Int)?
It’s a property completionHandler that has a closure type. The closure is optional, denoted by the ?, and the closure signature is (String) -> Int. This means the closure has one parameter of type String and returns one value of type Int.

Once more, in the secondary view controller, we call the closure when a button is tapped:

@IBAction func onButtonTap()
{
let result = completionHandler?(“FUS-ROH-DAH!!!”)

print(“completionHandler returns… \(result)”)
}

In the example above, this happens:

  • The closure completionHandler is called, with one string argument. Its result is assigned to result.
  • The result is printed out with print()

Then, in the MainViewController you can define the closure like this:

vc.completionHandler = { text in

print(“text = \(text)”)

return text.characters.count
}
This is the closure itself. It’s declared locally, so you can use all local variables, properties and functions.

In the closure the text parameter is printed out, and then the string length is returned as the result of the function.

This is where it gets interesting. The closure lets you pass data between view controllers bi-directionally! You can define the closure, work with the data that’s coming in, and return data to the code that invokes the closure.

You may note here that a function call, with delegation or a direct property, also allows you to return a value to the caller of the function. That’s absolutely true!

Closures might come in handy in the following scenarios:

  • You don’t need a complete delegation approach, with a protocol, you just want to create a quick function.
  • You want to pass a closure through multiple classes. Without a closure you’d have to create a cascading set of function calls, but with the closure you can just pass the block of code along.
  • You need to locally define a block of code with a closure, because the data you want to work with only exists locally.

One of the risks of using closures to pass data between view controllers is that your code can become very dense. It’s smartest to only use closures to pass data between view controllers if it makes sense to use closures over any other method – instead of just using closures because they’re so convenient!

So… what if you want to pass data between view controllers that don’t have, or can’t have, a connection between them?

Passing Data Between View Controllers with NotificationCenter

You can pass data between view controllers with Notification Center, via its NotificationCenter class.

The Notification Center handles notifications, and forwards incoming notifications to components that are listening for them. The Notification Center is the iOS SDK’s approach to the Observer-Observable software design pattern.

Quick Note: Since Swift 3, it’s called NotificationCenter – so no “NS” prefix. And keep in mind that these “notifications” aren’t push notifications.

Working with Notification Center has three key components:

  1. Observing the notification
  2. Sending the notification
  3. Responding to the notification

Let’s first start with observing the notification. Before you can respond to a notification, you need to tell the Notification Center that you want to observe it. The Notification Center then tells you about any notifications it comes across, because you’ve indicated you’re on the lookout for them.

Every notification has a name to identify them. In MainViewController you add the following static property to the top of the class:

static let notificationName = Notification.Name(“myNotificationName”)
This static property, also known as a class property, is available anywhere in the code by calling MainViewController.notificationName. This is how you identify the notification with one single constant. You wouldn’t want to mix up your notifications by mistyping it somewhere!

Here’s how you observe for that notification:

NotificationCenter.default.addObserver(self, selector: #selector(onNotification(notification:)), name: MainViewController.notificationName, object: nil)
You usually add this in viewDidLoad() or viewWillAppear(_:), so that the observation is registered when the view controller is put on screen. Here’s what happens in the code sample above:

  • You use NotificationCenter.default, which is the default Notification Center. You could create your own Notification Center, for instance for a certain kind of notifications, but chances are the default center is fine.
  • You then call the function addObserver(_:selector:name:object:) on the Notification Center.
  • The first argument is the instance that does the observation, and it’s almost always self.
  • The second argument is the selector you want to call when the notification is observed, and this is often a function of the current class.
  • The third parameter is the name of the notification, so you pass the static constant notificationName.
  • The fourth parameter is the object whose notifications you want to receive. You usually pass nil here, but you could use it to only observe notifications from one particular object.

At a later point you can stop observing the notification with this:

NotificationCenter.default.removeObserver(self, name: MainViewController.notificationName, object: nil)
You can also stop observing for all registered notifications with:

NotificationCenter.default.removeObserver(self)
Remember that notifications are explicit, so you always observe one type of notification that results in one function call on one object (usually self) when the notification occurs.

The function that will get called when the notification occurs is onNotification(notification:), so let’s add that to the class:

@objc func onNotification(notification:Notification)
{
print(notification.userInfo)
}
The @objc keyword is required since Swift 4, because the NSNotificationCenter framework is Objective-C code. In the function, you’ll simply print out the notification payload with notification.userInfo.

Then, posting the notification is easy. Here’s how you do that:

NotificationCenter.default.post(name: MainViewController.notificationName, object: nil, userInfo: [“data”: 42, “isImportant”: true])

Again, there’s a few moving parts:

  • You call the function post(name:object:userInfo:) on the default Notification Center, exactly the same center as you used before.
  • The first function argument is the name of the notification, that static constant that you defined before.
  • The second argument is the object posting the notification. You can often leave this nil, but if you’ve used the object argument when observing the notification you can pass the same object here to exclusively observe and post for that object.
  • The third argument is the notification payload called userInfo. You can pass a dictionary with any kind of data here. In this example, you’re passing some data and a boolean value.

That’s all there is to it!

The Notification Center comes in handy in a few scenarios:

  • The view controllers or other classes you want to pass data between are not closely related. Think about a table view controller that needs to respond when a REST API receives new data.
  • The view controllers don’t necessarily have to exist yet. It could happen that the REST API receives data before the table view is put on screen. Observing for notifications is optional, which is an advantage if parts of your app are ephemeral.
  • Many view controllers need to respond to one notification, or one view controller needs to respond to multiple notifications. Notification Center is many-to-many.

You can think of the Notification Center as a superhighway for information, where notifications are constantly sent over its lanes, in many directions and configurations.

If you just want some “local traffic” between view controllers, it doesn’t make sense to use Notification Center – you’d use a simple delegate, property or closure instead. But if you want to repeatedly and regularly send data from one part of your app to the other, Notification Center is a great solution.

You can learn more about working with NotificationCenter here: How To: Using Notification Center In Swift.

Conclusion

Passing data between view controllers is a fundamental aspect of iOS development, enabling seamless user experiences and dynamic interactions within apps. Whether you’re using properties, segues, delegation, or other techniques, it’s crucial to choose the approach that best fits the specific needs of your app.

In conclusion, while the mechanics of passing data might seem straightforward, the underlying decisions about which method to use can significantly impact the robustness and efficiency of your app. Always test thoroughly, consider the user experience, and strive for clean, maintainable code.

Abhinav Girdhar

Founder and CEO of Appy Pie

App Builder

Most Popular Posts