Data Passing Between iOS View Controllers

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)

  1. Declare a property in the destination view controller header:
@property (nonatomic, strong) NSString *incomingValue;
  1. 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];
}
  1. 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:

  1. Add a property to the source controller:
@property (nonatomic, strong) NSString *returnValue;
  1. 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];
}
  1. 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)

  1. Declare a protocol in the source controller header:
@protocol SourceControllerDelegate <NSObject>
@optional
- (void)passDataToDestination:(NSString *)data;
@end
  1. Add a delegate property to the source controller interface:
@property (nonatomic, weak) id<SourceControllerDelegate> delegate;
  1. In the destination controller header, adopt the protocol:
@interface DestinationViewController : UIViewController <SourceControllerDelegate>
  1. 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];
}
  1. Implement the delegate method in the destination controller:
- (void)passDataToDestination:(NSString *)data {
    self.displayLabel.text = data;
}

Reverse Delegate Passing (B → A)

  1. Declare a protocol in the destination controller header:
@protocol DestinationControllerDelegate <NSObject>
@optional
- (void)passDataBack:(NSString *)data;
@end
  1. Add a delegate property to the destination controller:
@property (nonatomic, weak) id<DestinationControllerDelegate> delegate;
  1. In the destination controller's button handler, invoke the delegate method before popping:
- (void)handleDelegateAction {
    [self.delegate passDataBack:@"ReturnDelegateData"];
    [self.navigationController popViewControllerAnimated:YES];
}
  1. In the source controller header, adopt the protocol:
@interface SourceViewController () <DestinationControllerDelegate>
  1. 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];
}
  1. 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)

  1. In the destination controller, override init to 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];
}
  1. 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)

  1. 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];
}
  1. 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)

  1. 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];
}
  1. 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)

  1. 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];
}
  1. 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)

  1. Declare a block property in the destination controller:
@property (nonatomic, strong) void (^dataHandler)(NSString *);
  1. Override init in 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;
}
  1. 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)

  1. Declare a block property in the destination controller:
@property (nonatomic, strong) void (^callbackHandler)(NSString *);
  1. 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];
}
  1. 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

Tags: iOS Objective-C View Controller Data Passing UIKit

Posted on Sun, 09 Aug 2026 16:19:37 +0000 by mfalomir