This article explores the various methods for passing data between view controllers in iOS development, covering property-based approaches, delegate patterns, notifications, singletons, and closures.
Environment Setup
Before implementing data passing, set up the navigation hierarchy in the application delegate. The following code initializes the window with a UINavigationController as the root:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
SourceViewController *rootVC = [[SourceViewController alloc] init];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:rootVC];
self.window.rootViewController = navController;
navController.navigationBar.translucent = NO;
[self.window makeKeyAndVisible];
return YES;
}
Create two view controllers: SourceViewController (A) and DestinationViewController (B). Each controller requires a button to trigger navigation and a label to display received data.
UI Configuration
In the source view controller's viewDidLoad, configure the interface elements:
self.view.backgroundColor = [UIColor blueColor];
UIButton *actionButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
actionButton.frame = CGRectMake(100, 60, 175, 40);
[actionButton setTitle:@"Property Passing" forState:UIControlStateNormal];
[actionButton addTarget:self action:@selector(handlePropertyAction) forControlEvents:UIControlEventTouchUpInside];
actionButton.backgroundColor = [UIColor greenColor];
[self.view addSubview:actionButton];
[self.view addSubview:self.displayLabel];
Implement lazy loading for the label:
- (UILabel *)displayLabel {
if (!_displayLabel) {
_displayLabel = [[UILabel alloc] initWithFrame:CGRectMake(100, 20, 175, 30)];
_displayLabel.backgroundColor = [UIColor whiteColor];
}
return _displayLabel;
}
Apply similar configuration to the destination view controller, using a different background color for visual distinction.
Property-Based Passing
Property-based passing involves defining publicly accessible properties on the destination view controller and assigning values during navigation.
Forward Passing (A → B)
- Declare a property in the destination view controller header:
@property (nonatomic, strong) NSString *incomingValue;
- In the source controller's button handler, instantiate the destination controller and assign the value:
- (void)handlePropertyAction {
DestinationViewController *destVC = [[DestinationViewController alloc] init];
destVC.incomingValue = @"DataFromSource";
[self.navigationController pushViewController:destVC animated:YES];
}
- In the destination controller's
viewDidLoad, display the received value:
self.displayLabel.text = self.incomingValue;
Reverse Passing (B → A)
Reverse property passing requires accessing the existing source controller instance:
- Add a property to the source controller:
@property (nonatomic, strong) NSString *returnValue;
- In the destination controller's button handler, retrieve the source controler from the navigation stack and assign the value:
- (void)handlePropertyAction {
SourceViewController *sourceVC = self.navigationController.viewControllers[0];
sourceVC.returnValue = @"ReturnData";
[self.navigationController popViewControllerAnimated:YES];
}
- Update the display in the source controller's
viewWillAppear:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.displayLabel.text = self.returnValue;
}
Delegate Pattern Passing
The delegate pattern enables loose coupling between view controllers for bidirectional communication.
Forward Delegate Passing (A → B)
- Declare a protocol in the source controller header:
@protocol SourceControllerDelegate <NSObject>
@optional
- (void)passDataToDestination:(NSString *)data;
@end
- Add a delegate property to the source controller interface:
@property (nonatomic, weak) id<SourceControllerDelegate> delegate;
- In the destination controller header, adopt the protocol:
@interface DestinationViewController : UIViewController <SourceControllerDelegate>
- In the source controller's button handler, set the delegate and invoke the delegate method:
- (void)handleDelegateAction {
DestinationViewController *destVC = [[DestinationViewController alloc] init];
self.delegate = destVC;
[self.delegate passDataToDestination:@"DelegateData"];
[self.navigationController pushViewController:destVC animated:YES];
}
- Implement the delegate method in the destination controller:
- (void)passDataToDestination:(NSString *)data {
self.displayLabel.text = data;
}
Reverse Delegate Passing (B → A)
- Declare a protocol in the destination controller header:
@protocol DestinationControllerDelegate <NSObject>
@optional
- (void)passDataBack:(NSString *)data;
@end
- Add a delegate property to the destination controller:
@property (nonatomic, weak) id<DestinationControllerDelegate> delegate;
- In the destination controller's button handler, invoke the delegate method before popping:
- (void)handleDelegateAction {
[self.delegate passDataBack:@"ReturnDelegateData"];
[self.navigationController popViewControllerAnimated:YES];
}
- In the source controller header, adopt the protocol:
@interface SourceViewController () <DestinationControllerDelegate>
- Set the destination controller's delegate in the source controller's button handler:
- (void)handleDelegateAction {
DestinationViewController *destVC = [[DestinationViewController alloc] init];
destVC.delegate = self;
[self.navigationController pushViewController:destVC animated:YES];
}
- Implement the delegate method in the source controller:
- (void)passDataBack:(NSString *)data {
self.displayLabel.text = data;
}
Notification Center Passing
NSNotificationCenter facilitates communication between components without direct coupling. Ensure observers are registered before posting notifications.
Forward Notification Passing (A → B)
- In the destination controller, override
initto register as an observer:
- (instancetype)init {
if (self = [super init]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:@"DataNotification"
object:nil];
}
return self;
}
- (void)handleNotification:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo;
self.displayLabel.text = userInfo[@"dataKey"];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"DataNotification" object:nil];
}
- In the source controller's button handler, post the notification:
- (void)handleNotificationAction {
DestinationViewController *destVC = [[DestinationViewController alloc] init];
NSNotification *notification = [NSNotification notificationWithName:@"DataNotification"
object:nil
userInfo:@{@"dataKey": @"NotificationData"}];
[[NSNotificationCenter defaultCenter] postNotification:notification];
[self.navigationController pushViewController:destVC animated:YES];
}
Reverse Notification Passing (B → A)
- In the source controller's
viewDidLoad, register as an observer:
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:@"ReturnNotification"
object:nil];
}
- (void)handleNotification:(NSNotification *)notification {
NSDictionary *userInfo = notification.userInfo;
self.displayLabel.text = userInfo[@"dataKey"];
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"ReturnNotification" object:nil];
}
- In the destination controller's button handler, post the notification before popping:
- (void)handleNotificationAction {
NSNotification *notification = [NSNotification notificationWithName:@"ReturnNotification"
object:nil
userInfo:@{@"dataKey": @"ReturnData"}];
[[NSNotificationCenter defaultCenter] postNotification:notification];
[self.navigationController popViewControllerAnimated:YES];
}
Singleton Pattern Passing
A singleton instance persists throughout the application lifecycle, making it suitable for sharing data across multiple controllers.
Singleton Implementation
// DataManager.h
#import <Foundation/Foundation.h>
@interface DataManager : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, strong) NSString *sharedName;
@property (nonatomic, assign) NSInteger sharedValue;
+ (instancetype)sharedInstance;
@end
// DataManager.m
#import "DataManager.h"
static DataManager *sharedInstance = nil;
@implementation DataManager
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[DataManager alloc] init];
});
return sharedInstance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
if (sharedInstance == nil) {
sharedInstance = [super allocWithZone:zone];
}
return sharedInstance;
}
- (instancetype)copyWithZone:(NSZone *)zone {
return sharedInstance;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return sharedInstance;
}
@end
Forward Singleton Passing (A → B)
- In the source controller's button handler, assign values to the singleton:
- (void)handleSingletonAction {
DestinationViewController *destVC = [[DestinationViewController alloc] init];
DataManager *manager = [DataManager sharedInstance];
manager.sharedName = @"SharedData";
[self.navigationController pushViewController:destVC animated:YES];
}
- In the destination controller's
viewDidLoad, read from the singleton:
- (void)viewDidLoad {
[super viewDidLoad];
DataManager *manager = [DataManager sharedInstance];
self.displayLabel.text = manager.sharedName;
}
Reverse Singleton Passing (B → A)
- In the destination controller's button handler, update the singleton before popping:
- (void)handleSingletonAction {
DataManager *manager = [DataManager sharedInstance];
manager.sharedName = @"ReturnedData";
[self.navigationController popViewControllerAnimated:YES];
}
- In the source controller's
viewWillAppear, read the updated value:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
DataManager *manager = [DataManager sharedInstance];
self.displayLabel.text = manager.sharedName;
}
Block Passing
Blocks provide a convenient mechanism for callback-based data passing between view controllers.
Forward Block Passing (A → B)
- Declare a block property in the destination controller:
@property (nonatomic, strong) void (^dataHandler)(NSString *);
- Override
initin the destination controller to configure the block:
- (instancetype)init {
if (self = [super init]) {
__weak typeof(self) weakSelf = self;
self.dataHandler = ^(NSString *value) {
weakSelf.displayLabel.text = value;
};
}
return self;
}
- In the source controller's button handler, invoke the block:
- (void)handleBlockAction {
DestinationViewController *destVC = [[DestinationViewController alloc] init];
if (destVC.dataHandler) {
dispatch_async(dispatch_get_main_queue(), ^{
destVC.dataHandler(@"BlockData");
});
}
[self.navigationController pushViewController:destVC animated:YES];
}
Reverse Block Passing (B → A)
- Declare a block property in the destination controller:
@property (nonatomic, strong) void (^callbackHandler)(NSString *);
- In the source controller's button handler, assign the block:
- (void)handleBlockAction {
DestinationViewController *destVC = [[DestinationViewController alloc] init];
destVC.callbackHandler = ^(NSString *value) {
self.displayLabel.text = value;
};
[self.navigationController pushViewController:destVC animated:YES];
}
- In the destination controller's button handler, invoke the block before popping:
- (void)handleBlockAction {
if (self.callbackHandler) {
dispatch_async(dispatch_get_main_queue(), ^{
self.callbackHandler(@"ReturnBlockData");
});
}
[self.navigationController popViewControllerAnimated:YES];
}
Method Comparison
| Method | Use Case | Direction | Complexity |
|---|---|---|---|
| Property | Simple data transfer | A→B, B→A | Low |
| Delegate | Formal protocols, reusable components | A→B, B→A | Medium |
| Notification | Multiple observers, loosely coupled | A→B, B→A | Medium |
| Singleton | Global shared state | A→B, B→A | Low |
| Block | Callbacks, inline logic | A→B, B→A | Medium |
Practical Guidelines:
- Property passing works well for straightforward A→B scenarios
- Delegate and block patterns are preferred for B→A communication
- Notifications should be used sparingly due to potential memory management issues
- Singleton pattern suits scenarios where multiple controllers require access to shared state