Comprehensive Guide to Passing Data Between View Controllers in iOS

Nov 14, 2025 · Programming · 12 views · 7.8

Keywords: iOS | Objective-C | Swift | Data Passing | View Controllers | MVC

Abstract: This article provides an in-depth analysis of various methods for passing data between view controllers in iOS, covering forward and backward data passing using properties, segues, delegates, blocks, and NotificationCenter. It includes detailed code examples in Objective-C and Swift, along with best practices for effective data management in MVC architecture.

Introduction

In iOS development, the Model-View-Controller (MVC) architecture is fundamental, and passing data between view controllers is a common challenge for beginners. This article addresses how to transfer data, such as user selections from a UITableViewController back to a previous data entry form, using various methods in Objective-C and Swift. Through step-by-step explanations and code examples, it helps readers grasp core concepts and apply them in practical development.

Forward Data Passing

Forward data passing involves sending data from one view controller to another when navigating forward, such as pushing a new view controller onto a navigation stack.

Direct Property Setting

In Objective-C, you can set a property in the destination view controller before presenting it. For example, to pass a BOOL value:

// ViewControllerB.h
@property (nonatomic, assign) BOOL isSomethingEnabled;

// ViewControllerA.m
#import "ViewControllerB.h"
ViewControllerB *viewControllerB = [[ViewControllerB alloc] initWithNibName:@"ViewControllerB" bundle:nil];
viewControllerB.isSomethingEnabled = YES;
[self.navigationController pushViewController:viewControllerB animated:YES];

In Swift, a similar approach can be used with direct instantiation:

// DestinationViewController.swift
var isSomethingEnabled: Bool = false

// SourceViewController.swift
if let destinationVC = storyboard?.instantiateViewController(withIdentifier: "DestinationViewController") as? DestinationViewController {
    destinationVC.isSomethingEnabled = true
    navigationController?.pushViewController(destinationVC, animated: true)
}

Using Segues

When using Storyboards, segues provide a structured way to pass data. Override the prepare(for:sender:) method in the source view controller.

In Objective-C:

// ViewControllerA.m
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"showDetailSegue"]) {
        ViewControllerB *controller = (ViewControllerB *)segue.destinationViewController;
        controller.isSomethingEnabled = YES;
    }
}

In Swift:

// SourceViewController.swift
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "showDetailSegue" {
        if let destinationVC = segue.destination as? DestinationViewController {
            destinationVC.isSomethingEnabled = true
        }
    }
}

Backward Data Passing

Backward data passing involves returning data from a view controller to its predecessor, commonly implemented using protocols and delegates or blocks.

Protocols and Delegates

In Objective-C, define a protocol in the destination view controller and set a delegate.

// ViewControllerB.h
@protocol ViewControllerBDelegate <NSObject>
- (void)addItemViewController:(ViewControllerB *)controller didFinishEnteringItem:(NSString *)item;
@end

@interface ViewControllerB : UIViewController
@property (nonatomic, weak) id <ViewControllerBDelegate> delegate;
@end

// ViewControllerB.m
- (void)someMethod {
    NSString *itemToPassBack = @"Pass this value back";
    [self.delegate addItemViewController:self didFinishEnteringItem:itemToPassBack];
}

// ViewControllerA.h
#import "ViewControllerB.h"
@interface ViewControllerA : UIViewController <ViewControllerBDelegate>

// ViewControllerA.m
- (void)addItemViewController:(ViewControllerB *)controller didFinishEnteringItem:(NSString *)item {
    NSLog(@"Received: %@", item);
}

// When pushing ViewControllerB
ViewControllerB *viewControllerB = [[ViewControllerB alloc] init];
viewControllerB.delegate = self;
[self.navigationController pushViewController:viewControllerB animated:YES];

In Swift, the approach is similar with protocols:

// DestinationViewController.swift
protocol DataDelegate: AnyObject {
    func sendData(data: String)
}

class DestinationViewController: UIViewController {
    weak var delegate: DataDelegate?
    
    func someMethod() {
        delegate?.sendData(data: "Hello")
    }
}

// SourceViewController.swift
class SourceViewController: UIViewController, DataDelegate {
    func sendData(data: String) {
        print("Received: \(data)")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        if let destinationVC = storyboard?.instantiateViewController(withIdentifier: "DestinationViewController") as? DestinationViewController {
            destinationVC.delegate = self
            navigationController?.pushViewController(destinationVC, animated: true)
        }
    }
}

Using Blocks

Blocks (or closures in Swift) provide a callback mechanism for passing data back.

In Objective-C:

// ViewControllerA.h
@property (nonatomic, copy) void (^selectedVoucherBlock)(NSString *);

// ViewControllerA.m
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.selectedVoucherBlock = ^(NSString *voucher) {
        weakSelf.someLabel.text = voucher;
    };
}

// When pushing ViewControllerB
ViewControllerB *viewControllerB = [[ViewControllerB alloc] init];
viewControllerB.sourceVC = self; // Assuming sourceVC is a property in ViewControllerB
[self.navigationController pushViewController:viewControllerB animated:YES];

// In ViewControllerB
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *voucher = vouchersArray[indexPath.row];
    if (self.sourceVC.selectedVoucherBlock) {
        self.sourceVC.selectedVoucherBlock(voucher);
    }
    [self.navigationController popViewControllerAnimated:YES];
}

In Swift, using closures:

// DestinationViewController.swift
var callback: ((String) -> Void)?

// SourceViewController.swift
if let destinationVC = storyboard?.instantiateViewController(withIdentifier: "DestinationViewController") as? DestinationViewController {
    destinationVC.callback = { data in
        print("Received: \(data)")
    }
    navigationController?.pushViewController(destinationVC, animated: true)
}

// In DestinationViewController, call the callback when needed
callback?("Data to pass back")

Other Methods

NSNotification Center

NSNotificationCenter allows for loosely coupled communication between objects.

In Objective-C:

// In the sending view controller
[[NSNotificationCenter defaultCenter] postNotificationName:@"handleDeepLinking" object:nil userInfo:@{@"data": @"Hello"}];

// In the receiving view controller
- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"handleDeepLinking" object:nil];
}

- (void)handleNotification:(NSNotification *)notification {
    NSString *data = notification.userInfo[@"data"];
    NSLog(@"Received: %@", data);
}

In Swift:

// In the sending view controller
NotificationCenter.default.post(name: Notification.Name("PassDataNotification"), object: nil, userInfo: ["data": "Hello"])

// In the receiving view controller
override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(self, selector: #selector(handleNotification(_:)), name: Notification.Name("PassDataNotification"), object: nil)
}

@objc func handleNotification(_ notification: Notification) {
    if let data = notification.userInfo?["data"] as? String {
        print("Received: \(data)")
    }
}

Comparison and Best Practices

Each method has its pros and cons. Direct property setting is simple but can lead to tight coupling; segues are ideal for Storyboard-based apps; protocols and delegates are standard for backward passing and promote loose coupling; blocks are flexible but can cause retain cycles if not handled carefully; NSNotificationCenter is useful for global events but can be overkill for simple data passing. Best practices include using delegates for one-to-one communication, blocks for callbacks, and avoiding global state like singletons to maintain modularity.

Conclusion

Passing data between view controllers is essential in iOS development. By understanding and applying methods like property setting, segues, delegates, blocks, and NotificationCenter, developers can choose the appropriate approach based on their app's architecture and needs. This guide provides comprehensive examples in both Objective-C and Swift to aid beginners and experienced developers alike.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.