Published on

How to Pass Data between View Controllers Like a Pro

Authors

Passing data between view controllers is a crucial aspect of building iOS applications. There are several ways to achieve this, such as using delegates, closures, or notifications. However, these approaches can become complex and hard to maintain when dealing with multiple view controllers and data sources.

In this article, we will discuss how to use the coordinator pattern to pass data between view controllers. The coordinator pattern is a design pattern that helps in managing the flow of an application by decoupling view controllers from each other and centralizing the navigation logic.

We will start by defining what the coordinator pattern is, and then we will demonstrate how it can be used to pass data between view controllers. Finally, we will highlight the benefits of using this approach in building iOS applications.

What is the Coordinator Pattern?

The coordinator pattern is a design pattern that helps in managing the flow of an application by decoupling view controllers from each other and centralizing the navigation logic. The coordinator acts as a mediator between the view controllers, providing a layer of abstraction to handle the transitions and data passing between them.

The coordinator pattern works by creating a coordinator class that is responsible for managing the flow of the application. The coordinator is responsible for creating, presenting, and dismissing view controllers, and passing data between them. Each view controller is assigned a coordinator, and the coordinator handles the navigation logic between them.

Using Coordinator Pattern to Pass Data Between View Controllers

To illustrate how to use the coordinator pattern to pass data between view controllers, we will create a simple application that displays a list of items and allows the user to add, edit, and delete items.

First, we will create a coordinator class that holds an array of items and manages the navigation between the view controllers. Here is an example of how the coordinator class could look like:

class MainCoordinator {
    var items = [Item]()
    var navigationController: UINavigationController

    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }

    func start() {
        let viewController = ItemListViewController.instantiate()
        viewController.coordinator = self
        navigationController.pushViewController(viewController, animated: false)
    }

    func addItem() {
        let viewController = AddItemViewController.instantiate()
        viewController.coordinator = self
        navigationController.pushViewController(viewController, animated: true)
    }

    func editItem(_ item: Item) {
        let viewController = AddItemViewController.instantiate()
        viewController.coordinator = self
        viewController.itemToEdit = item
        navigationController.pushViewController(viewController, animated: true)
    }

    func didFinishAddingItem(_ item: Item) {
        items.append(item)
        navigationController.popViewController(animated: true)
    }

    func didFinishEditingItem(_ item: Item) {
        if let index = items.firstIndex(where: { $0.id == item.id }) {
            items[index] = item
        }
        navigationController.popViewController(animated: true)
    }
}

In this example, we have created a MainCoordinator class that holds an array of items and manages the navigation between the view controllers. The coordinator has functions to add, edit, and delete items, as well as functions to handle the completion of adding and editing items.

The coordinator class also has a start function that initializes the ItemListViewController and sets the coordinator as its delegate. The start function is called from the AppDelegate to start the application.

Next, we will create two view controllers: ItemListViewController and AddItemViewController.

class ItemListViewController: UIViewController {
    var coordinator: MainCoordinator?
}

class AddItemViewController: UIViewController {
    var coordinator: MainCoordinator?
    var itemToEdit: Item?
}

The ItemListViewController has a delegate property that is set to the coordinator, and the AddItemViewController has a coordinator property and an optional itemToEdit property that is set if the user wants to edit an existing item.

Now that we have defined the coordinator and view controllers, we can see how data is passed between them using the coordinator pattern. When the user wants to add a new item, they will click the add button in the ItemListViewController, which will call the addItem function in the coordinator.

@objc func addButtonTapped() {
    coordinator?.addItem()
}

The addItem function creates an instance of the AddItemViewController, sets the coordinator as its delegate, and pushes it onto the navigation stack.

func addItem() {
    let viewController = AddItemViewController.instantiate()
    viewController.coordinator = self
    navigationController.pushViewController(viewController, animated: true)
}

When the user has finished adding the item, they will tap the save button in the AddItemViewController, which will call the didFinishAddingItem function in the coordinator.

@objc func saveButtonTapped() {
    guard let name = nameTextField.text, !name.isEmpty else {
        return
    }
    let item = Item(name: name, id: UUID())
    coordinator?.didFinishAddingItem(item)
}

The didFinishAddingItem function adds the new item to the array of items, and pops the AddItemViewController from the navigation stack.

func didFinishAddingItem(_ item: Item) {
    items.append(item)
    navigationController.popViewController(animated: true)
}

Similarly, when the user wants to edit an existing item, they will tap the item in the ItemListViewController, which will call the editItem function in the coordinator.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let item = coordinator?.items[indexPath.row]
    coordinator?.editItem(item)
}

The editItem function creates an instance of the AddItemViewController, sets the coordinator as its delegate, sets the itemToEdit property to the selected item, and pushes it onto the navigation stack.

func editItem(_ item: Item) {
    let viewController = AddItemViewController.instantiate()
    viewController.coordinator = self
    viewController.itemToEdit = item
    navigationController.pushViewController(viewController, animated: true)
}

When the user has finished editing the item, they will tap the save button in the AddItemViewController, which will call the didFinishEditingItem function in the coordinator.

@objc func saveButtonTapped() {
    guard let name = nameTextField.text, !name.isEmpty else {
        return
    }
    if let itemToEdit = itemToEdit {
        let editedItem = Item(name: name, id: itemToEdit.id)
        coordinator?.didFinishEditingItem(editedItem)
    }
}

The didFinishEditingItem function finds the edited item in the array of items using its ID, updates it with the new data, and pops the AddItemViewController from the navigation stack.

func didFinishEditingItem(_ item: Item) {
    if let index = items.firstIndex(where: { $0.id == item.id }) {
        items[index] = item
    }
    navigationController.popViewController(animated: true)
}

Benefits of Using the Coordinator Pattern

Using the coordinator pattern to pass data between view controllers has several benefits:

Simplicity: The coordinator pattern makes it easy to read and follow the data flow between view controllers. Each view controller has a clear separation of concerns, making the code easier to maintain and debug.

Decoupling: By using the coordinator pattern, we can decouple the view controllers from each other and centralize the navigation logic in a separate class. This allows us to modify the navigation flow without having to change the code in each view controller.

Reusable Components: Because the coordinator is responsible for managing the navigation flow, we can easily reuse view controllers in other parts of the app without having to rewrite the navigation code.

Testability: The coordinator pattern makes it easier to write unit tests for each view controller, as the navigation logic is handled separately in the coordinator.

Scalability: The coordinator pattern can be easily scaled to handle more complex navigation flows and multiple coordinators can be used to manage different parts of the app.

In conclusion

Using the coordinator pattern to pass data between view controllers is a simple, flexible, and scalable approach that allows us to decouple the navigation logic from the view controllers, making our code easier to read, maintain, and test. By centralizing the navigation code in a separate coordinator class, we can easily modify the navigation flow and reuse view controllers in different parts of the app without having to rewrite the navigation code.