When developers first start iOS development, a problem that will arise pretty quickly is “How do I pass data between my objects?”. Fortunately, there are many solutions for you to choose from, but this causes a problem in itself. Which one to choose for your situation? It is important that you pick the correct solution for what you are trying to achieve. Three techniques are Notifications, Delegates and Closures. To demonstrate these in action, I will demonstrate with a small scenario where we have a LoginViewController and an AuthenticationSession. Our LoginViewController will take the users credentials and pass them to the AuthenticationSession, which will respond with the users’ unique identifier. The big question is, how do we get our userIdentifier back to our LoginViewController?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: LoginViewController | |
class LoginViewController: UIViewController { | |
func login(with username: String, | |
password: String) { | |
let authenticationSession = AuthenticationSession() | |
authenticationSession.performLogin(with: username, | |
password: password) | |
} | |
} | |
// MARK: AuthenticationSession | |
class AuthenticationSession { | |
func performLogin(with username: String, | |
password: String) { | |
let userIdentifier = Service.login(username, password) | |
} | |
} |
Notifications:
A common and very flexible approach is using Notifications. To use Notifications we would simply have to add an observer to our LoginViewController object that wants to retrieve the userIdentifier and send a notification from the AuthenticationSession.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: LoginViewController | |
class LoginViewController: UIViewController { | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(handleLogin(notification:)), | |
name: Notification.Name("UserIdentifier"), | |
object: nil) | |
} | |
func login(with username: String, | |
password: String) { | |
let authenticationSession = AuthenticationSession() | |
authenticationSession.performLogin(with: username, | |
password: password) | |
} | |
@objc func handleLogin(notification: Notification) { | |
if let userInfo = notification.userInfo { | |
if let userIdentifier = userInfo["User Identifier"] as? String { | |
// Handle the login with userIdentifier | |
} | |
} | |
} | |
} | |
// MARK: AuthenticationSession | |
class AuthenticationSession { | |
func performLogin(with username: String, | |
password: String) { | |
let userIdentifier = "12345" //Service.login(username, password) | |
NotificationCenter.default.post(name: Notification.Name("UserIdentifier"), object: self, userInfo: ["User Identifier": userIdentifier]) | |
} | |
} |
Now you might be thinking, brilliant I can copy this pattern to solve the problem and I don’t even need to read the other two… BUT WAAAAIT! Using notifications for this situation is the worst of the three options, let me explain why. Notifications were designed to be used in one to many relationships, i.e you can set multiple observers anywhere in your app and when you fire one notification every object observing the notification will react. This makes notifications extremely powerful tools, however, they should always be used sparingly. Medium to large sized apps with many notifications firing for different events are a nightmare to debug and maintain. It is very easy to lose context of what objects are interacting with each other. If two objects have a one to one relationship with each other, as they do in our Login case then it is always the best option to use Delegates or Closures.
Delegates:
Delegates are another approach which are a great way of passing data between two loosely coupled objects. Delegates are a lot easier to debug and maintain than notifications. Our LoginViewController simply needs to conform to our AuthenticationSessionDelegate and then will be notified by our AuthenticationSession every time we performLogin.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: LoginViewController | |
class LoginViewController: UIViewController, AuthenticationSessionDelegate { | |
func login(with username: String, | |
password: String) { | |
let authenticationSession = AuthenticationSession() | |
authenticationSession.delegate = self | |
authenticationSession.performLogin(with: username, | |
password: password) | |
} | |
// MARK: AuthenticationSessionDelegate Methods | |
func authenticationSession(_ authenticationSession: AuthenticationSession, didLoginWith userIdentifier: String) { | |
// Handle the login with userIdentifier | |
} | |
} | |
// MARK: AuthenticationSessionDelegate | |
protocol AuthenticationSessionDelegate: class { | |
func authenticationSession(_ authenticationSession: AuthenticationSession, didLoginWith userIdentifier: String) | |
} | |
// MARK: AuthenticationSession | |
class AuthenticationSession { | |
var delegate: AuthenticationSessionDelegate? | |
func performLogin(with username: String, | |
password: String) { | |
let userIdentifier = "12345" //Service.login(username, password) | |
delegate?.authenticationSession(self, didLoginWith: userIdentifier) | |
} | |
} |
Closures:
Closures are another great option for us to consider in this scenario. Closures require the least amount of boilerplate code and are easy to follow a logical flow. Closures are a very good way of passing data between two objects that have a one to one relationship and only have one flow to handle. Here our LoginViewController waits until AuthenticationSession has finished, inside the completion the userIdentifer is transferred.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// MARK: LoginViewController | |
class LoginViewController: UIViewController { | |
func login(with username: String, | |
password: String) { | |
let authenticationSession = AuthenticationSession() | |
authenticationSession.performLogin(with: username, | |
password: password, | |
completion: { userIdentifier in | |
// Handle the login with userIdentifier | |
}) | |
} | |
} | |
// MARK: AuthenticationSession | |
class AuthenticationSession { | |
func performLogin(with username: String, | |
password: String, | |
completion: ((String) -> Void)) { | |
let userIdentifier = "12345" //Service.login(username, password) | |
completion(userIdentifier) | |
} | |
} |
Your situation will determine which one of these three techniques you use. All three are useful in different scenarios, so it is important to be comfortable to use each one. In our Login example, I would choose to use a closure. It is a great solution seeing as we are passing data between two objects in a one to one relationship. We also have only one completion option to handle. If we had more options I would consider using delegates instead. Because this is a one to one relationship, I would not choose to use Notifications.
To sum up, If you are passing data from one object to another, consider using delegates or closures first. However, if neither of those suit your needs, keep notifications open as an option as well.