iOS: Three ways to pass data from Model to Controller

Adopting MVC in iOS Project

Image credit: Stanford University CS193P, Fall-2010

If you are an iOS developer, or a software developer in general, you definitely solve this problem in almost every project: how to pass data from the Model to Controller.

This assumes, of course, that you are using MVC or MVVM pattern in your project. If all of your code for requesting, receiving, and parsing data, along with updating the UI is located within a single UIViewController subclass, you should probably adopt one of the iOS Architecture Patterns first.

I will describe three basic ways of passing the data back to your Controller:

  1. Using Callbacks
  2. Using Delegation
  3. Using Notifications

We will go through each of these three concepts in details, following my example step-by-step. By the end of this tutorial you will able to choose which one is the best fit for your project.

At the beginning, we will create a basic project that has ViewController and DataModel classes. At this step, it doesn’t matter what your data source is. It could be a local JSON file, a local image saved in the app directory, Core Data, or a HTTP response. In any case, once you receive data in your Data Model, you need a way to pass it to your View Controller.

So, you’ve created two classes: ViewController and DataModel:

class ViewController: UIViewController {
}

class DataModel {
}

Part 1. Callback as Completion Handler

This way is very easy to setup. First, we create a requestData method that takes completion (a block):

class DataModel {
    func requestData(completion: ((_ data: String) -> Void)) {

}
}
Completion here is a method, that takes a String as a data and has a Void return type.

Inside requestData, we run the code to request the data from any source:

class DataModel {
    func requestData(completion: ((_ data: String) -> Void)) {
       // the data was received and parsed to String
         let data = "Data from wherever"
    }
}

All we need to do now is to call completion with the data we have just received:

class DataModel {
   func requestData(completion: ((data: String) -> Void)) {
      // the data was received and parsed to String
let data = "Data from wherever"
      completion(data)
}
}

The next step is to create an instance of DataModel in ViewController class and call requestData method. In completion, we call a private method useData:

class ViewController: UIViewController {
   private let dataModel = DataModel()
   override func viewDidLoad() {
      super.viewDidLoad()
      dataModel.requestData { [weak self] (data: String) in
self?.useData(data: data)
}
}
   private func useData(data: String) {
print(data)
}
}
Note that we capture self as a weak variable inside the closure.

That’s it. Now you have your data in ViewController, while all the data-related code stays within the DataModel class. If you build and run the project, you will see the data string printed in the log.


Part 1.5. Callback as a class property

Another way of using a callback to communicate with ViewController is to create a callback as a DataModel property:

class DataModel {
      var onDataUpdate: ((_ data: String) -> Void)?
}

Now, inside the dataRequest method, instead of using a completion handler, we can use this callback:

func dataRequest() {
// the data was received and parsed to String
let data = "Data from wherever"

onDataUpdate?(data)
}

To use this callback in ViewController class, we simply assign an appropriate method to it (again using weak self):

class ViewController: UIViewController {
   private let dataModel = DataModel()
   override func viewDidLoad() {
super.viewDidLoad()
      dataModel.onDataUpdate = { [weak self] (data: String) in
self?.useData(data: data)
}
dataModel.requestData()
   }
}

You can also create a multiple callback properties (onDataUpdate, onHTTPError, etc). All callbacks are optional to use, so if you don’t need to do anything on onHTTPError, you simply do not use this callback. These are the benefits of this method compared with the previous one.


Part 2. Delegation

Delegation is the most common way to communicate between DataModel and ViewController.

protocol DataModelDelegate: class {
    func didRecieveDataUpdate(data: String)
}
The class keyword in the Swift protocol definition limits protocol adoption to class types (and not structures or enums). This is important if we want to use a weak reference to the delegate. We need be sure we do not create a retain cycle between the delegate and the delegating objects, so we use a weak reference to delegate (see below).

Now you need to create this weak delegate in DataModel:

weak var delegate: DataModelDelegate?

To call the delegate, we use it the same way as the callback method:

class DataModel {
      weak var delegate: DataModelDelegate?
      func requestData() {
         // the data was received and parsed to String
         let data = “Data from wherever”
         delegate?.didRecieveDataUpdate(data: data)
}
}

Create an instance of DataModel in ViewController, assign its delegate to self, and requestData:

class ViewController: UIViewController {
      private let dataModel = DataModel()
      override func viewDidLoad() {
super.viewDidLoad()
         dataModel.delegate = self
dataModel.requestData()
}
}

As the last step, create a ViewController extension, conform to DataModelDelegate protocol, and use didRecieveDataUpdate delegate method:

extension ViewController: DataModelDelegate {
      func didRecieveDataUpdate(data: String) {
         print(data)
}
}

Comparing to the callback way, Delegation pattern is easier to reuse across the app: you can create a base class that conforms to the protocol delegate and avoid code redundancy. However, delegation is harder to implement: you need to create a protocol, set the protocol methods, create Delegate property, assign Delegate to ViewController, and make this ViewController conform to the protocol. Also, the Delegate has to implement every method of the protocol by default.

If you want to have an optional delegate method, your protocol needs to be an @objc protocol.

Again, build and run the project to see data string printed in log.


Part 3. Notification

While the first two ways are very commonly used, the Notification way is not obvious.

Here is one of the possible scenarios where you may want to use Notifications to communicate between DataModel and ViewController. Let’s say you have a shared data source, and you want to use it across the app.

For example, if you needed to retrieve a lot of locally stored user images and use them in multiple ViewControllers, using delegation would require every single ViewController to conform to its protocol.

In this case, using callbacks or delegation is also possible, but Notification does the job in more elegant way.

First, we modify DataModel and make it a singleton class:

class DataModel {
   static var sharedInstance = DataModel()
   private init() { }
}

Next, we add a local variable to DataModel, that will store our data:

class DataModel {
   static var sharedInstance = DataModel()
private init() { }

private (set) var data: String?
}
We use private(set) access modifier, because we want this property to be read-only. The only way to modify this property is via the requestData() method in DataModel.

Finally, we implement the same requestData method as we used before:

class DataModel {
   static var sharedInstance = DataModel()
   private init() { }

private (set) var data: String?
   func requestData() {

   }
}

Once we receive data in requestData, we save it in a local variable:

func requestData() {
// the data was received and parsed to String
self.data = “Data from wherever”
}

After we updated the local data, we want to post a Notification. The best way to do this will be using a property observer. Add didSet property observer to data variable:

private (set) var data: String? {
didSet {

}
}

Before we post a notification, let’s create a meaningful name for it. We will create a string literal outside of DataModel class:

let dataModelDidUpdateNotification = “dataModelDidUpdateNotification”

Now we are ready to post a Notification:

private (set) var data: String? {
didSet {
NotificationCenter.default.post(name:
NSNotification.Name(rawValue: dataModelDidUpdateNotification), object: nil)
   }
}

Here is what is happening behind this code: property observer (as you may conclude from its name) will observe any changes in variable data. When those changes occur, we post a notification. Now we just need to add a listener to this notification in every ViewController that uses this data.

class ViewController: UIViewController {
     override func viewDidLoad() {
super.viewDidLoad()
         NotificationCenter.default.addObserver(self, selector: #selector(getDataUpdate), name: NSNotification.Name(rawValue: dataModelDidUpdateNotification), object: nil)
     }
}

Now the observer will listen to any updates in DataModel and call getDataUpdate method on every change. Let’s implement this method:

@objc private func getDataUpdate() {
      if let data = DataModel.sharedInstance.data {
print(data)
}
}

In this method we read DataModel.sharedInstance.data property. Note that we don’t create a local instance of DataModel here. Because DataModel is a singleton class, we access its methods and properties using a sharedInstance.

Comparing with Callbacks or Delegation, this notification does not actually pass any data from DataModel to ViewControllers: rather it informs everyone that new data is available. Instead of going to every ViewController and saying “Hey, here is the new data you’ve requested”, it sits at home and says something like “Hey everyone, I have new data available, you can come and grab it”.

When you deal with notifications, you should remember one thing: you need to remove an observer when it no longer needs to listen for notifications. In other words, we need to make sure that NotificationCenter is not managing any observers that are no longer capable of actively listening. To do this, we remove the appropriate observer when the ViewController is deallocated:

deinit {
      NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: dataModelDidUpdateNotification), object: self)
}
In some situations, you don’t want an observer to listen for notifications if the ViewController is still in Navigation Stack but is not currently visible. For example, when you present a second ViewController on top of the first one, updating the bottom one is waste of resources. In this case, you can add an observer on viewWillAppear and remove it on viewWillDisappear. This will assure that your ViewController is only listening for notifications when it’s currently on the screen.

The last step is to call requestData method, using a sharedInstance of DataModel:

class View Controller: UIViewController {
   override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(getDataUpdate), name: NSNotification.Name(rawValue: dataModelDidUpdateNotification), object: nil)
        DataModel.sharedInstance.requestData() 
}
}

Build and run the project to see exact the same results that you have in the previous parts.


These were three basic ways I use to pass data in my projects.

Do you use any other technics to pass data around? Feel free to leave a comment/question/opinion/advice below.

In my next article, I will give you a detailed example of using MVC with TableView.