Passing Data Between Views and View Controllers in iOS
Course: iOS App Development Training
Topic: Passing Data Between Views and View Controllers
Course Link: iOS App Development Course
Demo Link: Register for a Free Course Demo
Website: JustAcademy Official Website
1. Introduction
Passing data between views and view controllers is an essential concept in iOS application development. Most applications contain multiple screens that need to exchange information. For example, a user may select a product from a list, open its details screen, edit a profile, or submit a form. The information selected or entered on one screen must be transferred to another screen correctly.
In UIKit-based applications, data can be transferred using properties, initializers, delegation, closures, notifications, and shared data models. In SwiftUI, common approaches include property passing, bindings, observable models, and environment-based data sharing.
The best method depends on the relationship between the screens, the direction of data flow, and whether the receiving screen needs to send updates back.
2. Learning Objectives
- Understand views, view controllers, and data flow in iOS.
- Learn how to pass data using properties and initializers.
- Transfer data between UIKit view controllers.
- Understand delegation and closure-based communication.
- Pass data using segues and prepare(for:sender:) in storyboard-based projects.
- Use bindings and observable models in SwiftUI.
- Choose an appropriate communication method for different application requirements.
- Identify and troubleshoot common data-passing problems.
3. Understanding Views and View Controllers
3.1 What Is a View?
A view is a visual element displayed in an application's interface. In UIKit, views are generally instances of UIView or its subclasses. Examples include labels, buttons, text fields, image views, and custom interface components.
A view is primarily responsible for displaying content and responding to user interactions. A view can receive information from a view controller and present that information to the user.
3.2 What Is a View Controller?
A view controller, commonly implemented using UIViewController, manages a screen or a portion of an application's interface. It coordinates the screen's views, handles user interactions, and often manages the data required by those views.
For example, a product list view controller may store the available products, while a product detail view controller displays the product selected by the user.
3.3 Why Is Data Passing Necessary?
- Navigation: Send selected information to the next screen.
- Editing: Return updated information to a previous screen.
- Reusable components: Configure a custom view with different data.
- Form processing: Transfer entered information to a confirmation screen.
- Application consistency: Keep related screens synchronized with the correct data.
4. Common Data-Passing Techniques
| Technique |
Typical Use Case |
Direction |
| Property assignment |
Send information to a destination screen or view |
Usually one-way |
| Initializer injection |
Provide required data when creating an object |
Usually one-way |
| Storyboard segue |
Pass data during storyboard-based navigation |
Usually forward |
| Delegation |
Notify another object about an event or result |
Often backward |
| Closures |
Return a value or report an event through a callback |
One-way callback |
| Shared model or observable object |
Share state across multiple screens |
Potentially two-way or shared |
| NotificationCenter |
Broadcast events to multiple interested objects |
Event-based |
| SwiftUI Binding |
Allow a child view to read and update source-owned state |
Two-way access to a value |
5. Passing Data Through Properties in UIKit
Property assignment is one of the simplest methods for transferring data between view controllers. The source view controller creates or obtains the destination view controller, assigns the required values to its properties, and then presents or pushes it onto the navigation stack.
5.1 Example: Passing a Username to Another Screen
First, define the destination view controller with a property that receives the username.
import UIKit
class ProfileViewController: UIViewController {
var username: String = ""
override func viewDidLoad() {
super.viewDidLoad()
print("Received username: \(username)")
}
}
Next, assign the property before displaying the destination screen.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
if let profileVC = storyboard.instantiateViewController(
withIdentifier: "ProfileViewController"
) as? ProfileViewController {
profileVC.username = "Aarav"
navigationController?.pushViewController(
profileVC,
animated: true
)
}
The storyboard scene must use the identifier ProfileViewController, and it must be configured as the intended destination. A navigation controller is required for the push operation.
5.2 Advantages
- Easy to understand and implement.
- Suitable for small amounts of data.
- Works well for simple forward navigation.
5.3 Limitations
- The receiving object must expose an appropriate property.
- Required data may accidentally be left unassigned when properties have default values.
- Property assignment alone does not automatically communicate changes back to the source screen.
6. Passing Data Through Initializers
Initializer injection passes required information when an object is created. It can make dependencies explicit and help prevent a destination screen from being created without essential data.
6.1 Example
import UIKit
struct Product {
let name: String
let price: Double
}
class ProductDetailViewController: UIViewController {
let product: Product
init(product: Product) {
self.product = product
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(product:) to create this view controller.")
}
override func viewDidLoad() {
super.viewDidLoad()
print(product.name)
print(product.price)
}
}
Create the destination with the required product:
let product = Product(name: "Wireless Headphones", price: 2499.0)
let detailVC = ProductDetailViewController(product: product)
navigationController?.pushViewController(detailVC, animated: true)
Important: This initializer pattern is appropriate when the view controller is created programmatically. Storyboard-based view controllers are normally created through their coder-based initialization process, so required data is often supplied using a property or another storyboard-compatible mechanism.
7. Passing Data Using Storyboard Segues
A segue defines a transition between scenes in a storyboard. When the user navigates to another scene, UIKit can call prepare(for:sender:) on the source view controller before the transition is completed.
7.1 Destination View Controller
import UIKit
class OrderDetailViewController: UIViewController {
var orderID: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
print("Order ID: \(orderID)")
}
}
7.2 Source View Controller
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowOrderDetails",
let destination = segue.destination
as? OrderDetailViewController {
destination.orderID = 105
}
}
To use this approach:
- Create the source and destination scenes in the storyboard.
- Connect the appropriate segue between them.
- Set the segue identifier to
ShowOrderDetails.
- Declare a property in the destination to receive the data.
- Assign the value in
prepare(for:sender:).
- Run the application and verify that the destination receives the correct value.
If the segue destination is a navigation controller rather than the content view controller, access the intended child controller through the navigation controller's topViewController or another appropriate reference.
8. Passing Data Between a View Controller and Its Views
A view controller commonly updates its interface by assigning values to view properties. For example, a label can display a value received from another screen.
import UIKit
class WelcomeViewController: UIViewController {
@IBOutlet weak var welcomeLabel: UILabel!
var username = "Guest"
override func viewDidLoad() {
super.viewDidLoad()
welcomeLabel.text = "Welcome, \(username)!"
}
}
In this example, the view controller receives or stores the username and updates the label when its view loads. The label's outlet must be connected correctly in Interface Builder.
8.1 Passing Data to a Custom UIView
For reusable interface components, expose a configuration method or property rather than making the custom view depend directly on a particular view controller.
import UIKit
class UserCardView: UIView {
let nameLabel = UILabel()
func configure(name: String) {
nameLabel.text = name
}
}
The owning view controller can call userCardView.configure(name:) when the required information becomes available. The custom view remains focused on presentation instead of navigation or application-wide business logic.
9. Returning Data Using Delegation
Delegation is a common UIKit communication pattern. One object defines a protocol, while another object adopts that protocol and implements its methods. The delegated object calls the protocol method when an event or result needs to be reported.
Delegation is especially useful when a second screen must return edited data to the first screen without directly depending on the first screen's concrete class.
9.1 Define a Delegate Protocol
import UIKit
protocol EditNameDelegate: AnyObject {
func didUpdateName(_ name: String)
}
9.2 Implement the Delegate in the Destination
class EditNameViewController: UIViewController {
weak var delegate: EditNameDelegate?
func saveName(_ name: String) {
delegate?.didUpdateName(name)
navigationController?.popViewController(animated: true)
}
}
9.3 Receive the Result in the Source
class ProfileViewController: UIViewController, EditNameDelegate {
var currentName = "Aarav"
func openEditor() {
let editor = EditNameViewController()
editor.delegate = self
navigationController?.pushViewController(
editor,
animated: true
)
}
func didUpdateName(_ name: String) {
currentName = name
print("Updated name: \(currentName)")
}
}
In a complete interface, connect the editor's save action to saveName(_:) using the value entered by the user.
9.4 Why Use a Weak Delegate?
When the delegate is a class instance and the relationship could otherwise form a strong reference cycle, declaring it as weak helps prevent that cycle. The protocol therefore inherits from AnyObject, which restricts conforming types to reference types.
10. Passing Data Using Closures
A closure can be used as a callback that returns a value or reports an event. This is often convenient when the communication involves one result and does not need a separate protocol.
10.1 Example: Returning an Edited Name
import UIKit
class EditProfileViewController: UIViewController {
var onSave: ((String) -> Void)?
func saveProfile(name: String) {
onSave?(name)
navigationController?.popViewController(animated: true)
}
}
Configure the callback from the source screen:
let editor = EditProfileViewController()
editor.onSave = { [weak self] updatedName in
self?.title = updatedName
}
navigationController?.pushViewController(editor, animated: true)
The closure can return a string, an object, or another suitable result. Capture lists such as [weak self] are useful when a stored closure might otherwise retain its owner and create a reference cycle.
11. Passing Data with SwiftUI Views
SwiftUI uses a declarative approach to building interfaces. Views are value types, and data flow is commonly managed through properties, state, bindings, and observable models.
11.1 Passing a Value Through a View Property
import SwiftUI
struct GreetingView: View {
let username: String
var body: some View {
Text("Welcome, \(username)!")
}
}
struct HomeView: View {
var body: some View {
GreetingView(username: "Aarav")
}
}
The parent creates GreetingView and passes the username through its initializer. This is suitable for data that the child view reads but does not directly modify.
11.2 Sharing Editable Data with @Binding
Use @Binding when a child view needs to read and update a value owned by its parent. The parent typically stores the source of truth using @State.
import SwiftUI
struct NameEditorView: View {
@Binding var username: String
var body: some View {
TextField("Enter name", text: $username)
.textFieldStyle(.roundedBorder)
.padding()
}
}
struct ProfileScreen: View {
@State private var username = "Aarav"
var body: some View {
VStack {
Text("Profile: \(username)")
NameEditorView(username: $username)
}
.padding()
}
}
The dollar sign in $username provides a binding to the state value. When the text field changes the bound value, the parent and its dependent views update automatically.
11.3 Sharing State Through an Observable Model
When multiple SwiftUI views need access to shared application data, an observable model can keep that data in one place. In modern Swift, the Observation framework provides the @Observable macro. A parent can own an observable model using @State and pass it to child views.
import SwiftUI
import Observation
@Observable
class UserProfile {
var username = "Aarav"
var email = "[email protected]"
}
struct ProfileSummaryView: View {
var profile: UserProfile
var body: some View {
VStack {
Text(profile.username)
Text(profile.email)
}
}
}
struct ProfileContainerView: View {
@State private var profile = UserProfile()
var body: some View {
ProfileSummaryView(profile: profile)
}
}
This example demonstrates shared model access. In a larger application, the model can contain validation rules and business logic, while individual views focus on displaying and editing its values. For older deployment targets or projects using Combine, alternatives include ObservableObject and @Published.
12. Passing Data Through NotificationCenter
NotificationCenter broadcasts notifications to registered observers. It can be helpful when multiple unrelated components need to react to the same event, such as a user logging out or application data changing.
12.1 Posting a Notification
import Foundation
extension Notification.Name {
static let profileDidUpdate = Notification.Name("profileDidUpdate")
}
NotificationCenter.default.post(
name: .profileDidUpdate,
object: nil,
userInfo: ["username": "Aarav"]
)
12.2 Observing a Notification
let observer = NotificationCenter.default.addObserver(
forName: .profileDidUpdate,
object: nil,
queue: .main
) { notification in
let username = notification.userInfo?["username"] as? String
print("Profile updated: \(username ?? "Unknown")")
}
Store the observer token when using the block-based API and remove it when the observer no longer needs notifications. Avoid using notifications for every simple parent-child interaction because they can make data flow harder to trace.
13. Selecting the Appropriate Technique
| Requirement |
Recommended Approach |
Reason |
| Send a selected item to a details screen |
Property or initializer |
Clear and direct data flow |
| Transfer data through a storyboard transition |
Segue and prepare(for:sender:) |
Fits storyboard navigation |
| Return a form result to the previous controller |
Delegate or closure |
Provides an explicit callback |
| Pass read-only data to a SwiftUI child |
Stored property |
Simple one-way input |
| Let a SwiftUI child edit parent-owned state |
@Binding |
Shares access to the same value |
| Share application data across several screens |
Shared model or observable model |
Centralizes related state |
| Broadcast an event to unrelated observers |
NotificationCenter |
Supports event-based communication |
14. Common Errors and Troubleshooting
14.1 The Destination Receives an Empty Value
Possible cause: The destination property was never assigned, or it was assigned after the screen attempted to display the value.
Solution: Set the value before pushing or presenting the destination, and verify that the correct destination instance is being configured.
14.2 The Storyboard Segue Does Not Pass Data
Possible cause: The segue identifier is incorrect, the destination type check fails, or the actual destination is a navigation controller.
Solution: Verify the segue identifier, destination class, storyboard connections, and navigation-controller hierarchy.
14.3 An IBOutlet Is Nil
Possible cause: The outlet is not connected, the wrong storyboard scene is being used, or the view has not loaded yet.
Solution: Check the Interface Builder connection and update the outlet after the view has loaded. Do not assume an outlet is ready before the view controller's view is loaded.
14.4 Delegate Method Is Not Called
Possible cause: The delegate property was not assigned, the object does not conform to the protocol, or the callback is never invoked.
Solution: Set the delegate before navigation, confirm protocol conformance, and call the delegate method when the relevant event occurs.
14.5 SwiftUI Child Changes Do Not Appear in the Parent
Possible cause: A plain value was passed instead of a binding, or the state is owned by a different view than expected.
Solution: Use @Binding for a child that must edit parent-owned state, and pass the binding with the dollar-sign syntax.
14.6 Unexpected Memory Retention
Possible cause: A stored closure strongly captures its owner, or two objects hold strong references to each other.
Solution: Review object ownership and use weak references where appropriate. Ensure callbacks and observers are released when they are no longer required.
15. Best Practices for Data Passing
- Choose a technique based on the direction and lifetime of the data.
- Use descriptive property names such as
selectedProduct or updatedUsername.
- Prefer clear, typed models over loosely structured dictionaries for important application data.
- Use initializer injection when required data must exist at object creation time.
- Use delegation or closures for results that need to be returned to another object.
- Keep reusable views focused on presentation and avoid unnecessary dependencies on specific view controllers.
- Use SwiftUI state and bindings to maintain a clear source of truth.
- Avoid global variables for routine screen-to-screen communication.
- Use notifications for broad event communication rather than simple direct relationships.
- Validate user-entered data before saving or displaying it.
- Consider reference cycles when storing closures or delegates.
- Test navigation paths, empty values, updated values, and cancellation scenarios.
16. Practical Exercise: Build a Profile Editing Flow
Objective: Create a profile screen that opens an edit screen, accepts a new username, and updates the original screen when the user saves the change.
Requirements
- Create a profile screen containing a username label and an Edit button.
- Create an edit screen containing a text field and a Save button.
- Pass the existing username from the profile screen to the editor.
- Allow the user to modify the username.
- Return the updated value using a delegate or closure.
- Update the profile label when the editor closes.
- Test saving a new value, saving an unchanged value, and cancelling the edit.
Expected Result
When the profile initially displays “Aarav,” opening the editor should show “Aarav” in the text field. If the user changes it to “Aarav Sharma” and saves, the profile screen should display the updated name after returning.
Extension Task
Implement the same feature in SwiftUI using @State and @Binding. Compare how UIKit delegation and SwiftUI bindings handle the communication between screens.
17. Interview Questions and Answers
Q1. What does passing data between view controllers mean?
It means transferring information from one view controller to another so the destination can display or process the information needed for its screen.
Q2. What is the simplest way to pass data between UIKit view controllers?
Property assignment is a simple option. The source assigns a value to a property on the destination before presenting or pushing it.
Q3. What is initializer injection?
Initializer injection supplies required data when an object is created. It makes required dependencies explicit and can prevent invalid object creation.
Q4. What is prepare(for:sender:) used for?
It is a UIKit method used to configure a destination view controller before a storyboard segue transition completes, including assigning data to destination properties.
Q5. What is delegation?
Delegation is a communication pattern in which one object calls methods defined by a protocol to notify another object about an event or result.
Q6. When should closures be used for data passing?
Closures are useful when one object needs to return a result or report an event through a callback without introducing a separate delegate protocol.
Q7. What is the difference between @State and @Binding?
@State stores state owned by a SwiftUI view, while @Binding provides another view with access to read and modify a value owned elsewhere.
Q8. When is NotificationCenter useful?
It is useful for broadcasting events to multiple observers that do not necessarily have a direct parent-child relationship.
Q9. Why should global variables be avoided for routine data passing?
Global variables can create hidden dependencies, make testing harder, and allow unrelated parts of an application to modify shared values unexpectedly.
Q10. How can memory leaks occur during data passing?
Strong reference cycles can occur when objects or stored closures retain one another. Appropriate ownership rules and weak captures can help prevent these cycles.
18. Quick Revision Checklist
- Understand the difference between a view and a view controller.
- Know how to pass data using properties and initializers.
- Understand storyboard segues and
prepare(for:sender:).
- Know when to use delegation or closures to return data.
- Understand how a view controller configures its views.
- Use SwiftUI properties for read-only inputs and bindings for editable parent-owned values.
- Recognize when shared models or notifications are appropriate.
- Check storyboard identifiers, outlets, callback assignments, and memory ownership.
- Practice implementing a profile-editing or product-details workflow.
19. Conclusion
Passing data between views and view controllers is a fundamental skill for building interactive iOS applications. UIKit offers property assignment, initializer injection, storyboard segues, delegation, and closures for communicating between screens. SwiftUI provides property-based data flow, state, bindings, and observable models. NotificationCenter can be used when unrelated components need to respond to shared events.
By selecting the right technique for each situation, developers can build applications that are easier to understand, maintain, test, and extend. Practice with a multi-screen profile or product application to understand how data moves through a complete user workflow.
20. Course Resources