Long time no see dear readers and Appcoda fans! It’s been a long time since my last post here at Appcoda, and I am really glad to be back!
Today, I’m going to discuss about an important and definitely interesting topic which focuses on a quite often overlooked concept. And, this question is commonly asked by beginners:
How do you implement communication internally in apps and exchange messages properly among classes or other entities in Swift projects?
Undoubtedly, it is a must-have knowledge for every developer, new or not. Knowing what the available tools and techniques are, what works best, where and when, is crucial for a proper development process. After all, being able to send messages and data back and forth among classes is not something that one can optionally learn; instead, it’s one of the first things a developer should know about.
So, in this post we are going to see the following methods that allow to perform in-app communication:
- Delegation pattern
- Notifications
- Closures & action handlers
Usually, not all these three concepts are met together in one post, but one can find a lot about each topic separately. I felt the need to write a tutorial that concentrates them all, as they actually serve the same purpose; to let us exchange messages and data among our programming structures and entities. Through simple words and practical programming examples I will try my best to make it easily understandable how each concept works. Hopefully, by the end of the tutorial some valuable lessons will have been taught.
A programming tutorial without a bit of coding doesn’t sound right, so we will work on a demo application that will let us meet each concept as thoroughly as possible given the limits of a single post. For sure, what you will see next are standard techniques which you just have to adjust and use them in your projects. And by having said that, let’s not lose any more time here and let’s move on to the actual interesting stuff! Happy reading!
A Starter Project To… Start With
We are meant to make our hands dirty, but we won’t write a full app from the beginning. We will focus on the important stuff only, as all the rest have been already made in a starter project that you should get from here.
Supposing that you have downloaded the starter project and opened it in Xcode at this point, it’s time for a quick tour. Our main objective today is to build a shopping list app as much featured as possible, so we can apply all principles we will learn about delegates, notifications and closures. By the end of this tutorial, the demo app will be capable of:
- Creating shopping lists.
- Keeping and displaying a collection of all shopping lists added to the app.
- Selecting a shopping list and displaying its items.
- Editing an existing shopping list.
- Renaming & deleting shopping lists.
- Adding, editing, and deleting shopping list items.
If you run the starter project, you won’t see much happening. Yes, you can navigate from one view controller to another, but no data can be saved, nor there is other significant operation you can actually test. And that’s because vital parts regarding communication among app classes is still missing.
It would be more interesting to go through the code though. First of all, you will find three different view controllers:
AllListsViewController
: This is the place where all shopping lists are gathered and displayed. From this view controller, you can initiate the creation of a new list, editing an existing one, renaming and deleting.ShoppingListViewController
: It lays out all items of a list, and allows to create, edit, and delete items.EditItemViewController
: Its name suggests what is all about. You create, edit or delete shopping list items.
Additionally, there is a file called ShoppingList.swift which contains two structs:
ShoppingList
: It represents a shopping list programmatically and it contains properties for: id, name, edited timestamp & items of a list.ShoppingListManager
: It manages a collection ofShoppingList
items and it offers a variety of useful functions. An instance of this struct is used inAllListsViewController
view controller. It is calledlistManager
and it’s our tool to handle shopping lists.
You will also find a file named NotificationNameExtension.swift. This file is currently empty, but soon enough that’s going to change. More details later.
Finally, there are custom cells and custom views implementations. What is particularly interesting among them, is the RenameListView
, a custom view that allows to rename a shopping list. Well, not yet, but at some point will do. Keep it in mind, as we will work on it when we’ll be discussing about closures and action handlers.
Take your time to familiarize yourself with the starter project. Go through the code, examine the view controllers and other classes or structs, and find out what is offered in the starter project and what not. When you feel ready, keep reading. Delegation pattern comes first!
Delegation Pattern in Simple Words
Consider the following two classes, ClassA
and ClassB
.
ClassA
initializes an object of ClassB
, aiming to use services that ClassB
has to offer. We know that passing messages from ClassA
to ClassB
is straightforward, as long as there are public properties or methods to access. For example:
class ClassA {
init() {
super.init()
let classBObject = ClassB()
classBObject.someProperty = 5
classBObject.runThis(withFlag: true)
}
}
The troubling part is: How do we pass messages from ClassB
back to ClassA
?
And here’s where delegation pattern gets into play! Let’s go through the steps involved in implementing it.
Step One
The first step required in order to send messages from ClassB
back to ClassA
is to implement a custom protocol. It will contain functions that are meant to be:
- Implemented by
ClassA
so they perform actions specific to that class. - Called by
ClassB
, soClassB
can trigger actions inClassA
or pass data to it through the function parameter values.
protocol ClassBDelegate {
func dummyFunction()
func dummyFunction(withParameter param: String)
}
As a common convention, such a protocol has the delegate word as a suffix (that’s not mandatory, but recommended for clarity in code). Also, it usually starts with the name of the class it regards.
Step Two
ClassB
declares a property which is of the custom protocol’s type:
class ClassB {
var delegate: ClassBDelegate!
// ... more stuff
}
Once again for convention reasons, delegate
property is named like that for making the code comprehensive and easily understandable.
The truth is that it can be named any way you want, such as myDelegate
, customDelegate
, monday
, whatIsThis
, whatAFreakingGreatCode
, iLoveDelegates
and so on – Ok, just don’t do that!
Step Three
ClassA
adopts the new custom protocol, and implements its functions:
extension ClassA: ClassBDelegate {
func dummyFunction() {
// Do something...
}
func dummyFunction(withParameter param: String) {
// Do something and use param...
}
}
Step Four
ClassA
assigns itself to the delegate
property of ClassB
. In other words, it’s becoming ClassB
‘s delegate.
class ClassA {
init() {
// ...
classBObject.delegate = self
}
}
Step Five
ClassB
can use protocol’s functions to communicate with its delegate, which is ClassA
. A piece of advice: Always make sure that the delegate
property is not nil.
if let delegate = delegate {
delegate.dummyFunction()
}
That’s the roadmap to implement the delegation pattern. In the hypothetical example above I mentioned two classes only, but to be honest and more precise, any class that adopts the ClassBDelegate
protocol and assigns itself to the delegate
property of ClassB
can become ClassB
‘s delegate, and therefore establish a bi-directional communication.
Delegates in Action
Let’s see how delegation works in action, and let’s jump straight into our demo application. We will apply the steps outlined above in order to make it possible to:
- Add a new item created in the
EditItemViewController
to theitems
collection of theshoppingList
property in theShoppingListViewController
. - Update an edited item in
EditItemViewController
to theitems
collection of theshoppingList
property in theShoppingListViewController
. - Remove an item from the
items
collection of theshoppingList
property inShoppingListViewController
that had been previously deleted in theEditItemViewController
.
We will start by creating a new custom protocol that we’ll name EditItemViewControllerDelegate
. And we will do that in the EditItemViewControllerDelegate.swift file.
The first protocol’s function will let EditItemViewController
tell ShoppingListViewController
that a new item should be added to the items
collection:
protocol EditItemViewControllerDelegate {
func shouldAdd(item: String)
}
However, EditItemViewController
is unable to access any EditItemViewControllerDelegate
functions without having declared a delegate property in it first. So, let’s do that as the next step:
class EditItemViewController: UIViewController {
var delegate: EditItemViewControllerDelegate!
// Rest implementation...
}
Before we make use of the delegate
property, let’s open the ShoppingListViewController.swift file. We will make ShoppingListViewController
class adopt the EditItemViewControllerDelegate
protocol and subsequently implement its function. In the function’s body we’ll append the given item to the items
collection of the shoppingList
object, and we’ll refresh the table view so the new item gets displayed.
So, in the ShoppingListViewController.swift file go after the closing of the ShoppingListViewController
class. Add the following extension:
extension ShoppingListViewController: EditItemViewControllerDelegate {
func shouldAdd(item: String) {
shoppingList.items.append(item)
tableView.reloadData()
}
}
With the above addition, we just made it possible to save and display a new shopping list item created in the EditItemViewController
. But we’re not okay yet, as we haven’t set ShoppingListViewController
class as the delegate of the EditItemViewController
. To do so, go to the prepare(for:sender:)
method, and append the editItemVC.delegate = self
as shown next:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let identifier = segue.identifier {
if identifier == "idShowEditItemViewControllerSegue" {
if let editItemVC = segue.destination as? EditItemViewController {
editItemVC.delegate = self
// Rest of content...
}
}
}
}
You might be wondering why making self
the delegate of the EditItemViewController
takes place in the prepare(for:sender:)
method. Well, that’s because prepare(for:sender:)
is invoked when a segue is triggered, and it’s the place where we make any configuration regarding the view controller that’s about to be presented, before it gets presented.
To make that a bit more clear, EditItemViewController
is presented when it is asked to do so in two cases: When adding a new item and when editing one. The following line is the responsible for that in both:
performSegue(withIdentifier: "idShowEditItemViewControllerSegue", sender: self)
Now, right between the action that makes the above line to be executed and the actual presentation of the EditItemViewController
, it’s the prepare(for:sender:)
method that can be employed to do custom stuff regarding the upcoming view controller. And so we do.
Back to the EditItemViewController.swift file, where we are ready to make the Save button work. To refresh our memory, our goal is to type an item’s name in the textfield, and by tapping on the Save button to send that item to the ShoppingListViewController
, so it gets added to the items collection and eventually be displayed to the table view.
Head to the saveItem(_:)
IBAction method and update it as follows:
@IBAction func saveItem(_ sender: Any) {
guard let text = textField.text else { return }
if text != "" {
if let delegate = delegate {
delegate.shouldAdd(item: text)
}
navigationController?.popViewController(animated: true)
}
}
What happens here:
- At first, we make sure that user has typed something on the textfield, and they are not just trying to save while item’s name is being missing.
- Next, and that’s important, we make sure that the
delegate
property is not nil. - Then, we call the
shouldAdd(item:)
function of theEditItemViewControllerDelegate
protocol, providingtext
as the argument that represents the newly added item. - Finally, we dismiss the view controller by popping it from the navigation stack and we go back to our shopping list.
That’s it! The delegation pattern has been successfully applied, as the EditItemViewController
class can now talk back to the ShoppingListViewController
class by letting it know that there is a new item added, and by handing off that item at the same time so it can be further handled.
Feel free to test the app now, and create fake shopping list items. Each and every item you add is getting saved when you tap on the Save button!
But… wait a minute. What if we add twice or more times the same item?
Using Delegate Methods To Collect data
From a user’s point of view, the app shouldn’t add an item if it already exists. It doesn’t make sense to have a shopping list in which we can add milk 15 times, or tomatoes 10 times.
Let’s try to fix that. But the problem is: We have no information at all whether the item exists already or not at the time of saving, such that we can avoid adding it to the shopping list again.
Trying to deal with that by passing the entire items collection from ShoppingListViewController
to EditItemViewController
is obviously a really bad idea, totally counter-performant and a bad practice. So, what can we do?
In the majority of the times that you will create delegation-related protocols, their functions will not return any value. The one and only delegate function we implemented so far (shouldAdd(item:)
) is the best example of the kind of functions you will most probably be creating in such protocols. But, delegate functions can return a value which comes from the class that adopts the protocol and has been set as the delegate class. In other words and regarding our demo specifically, such a delegate function could return data straight from the ShoppingListViewController
class.
Aha! Here we are!
Let’s ask ShoppingListViewController
if the item we are trying to save already exists or not prior to saving it, and if it doesn’t exist then we allow it to be saved, otherwise we don’t!
Now we know what to do, let’s do it! Back to the EditItemViewControllerDelegate
protocol, let’s append the following function:
protocol EditItemViewControllerDelegate {
// ...
func isItemPresent(item: String) -> Bool
}
That function returns a Bool value. When true, the item we’re trying to add exists already so we just don’t keep it. When false, the item doesn’t exist and we can append it to the items collection.
Time to switch back to ShoppingListViewController.swift file, where we need to implement that function. Simply enough, we check for the item existence and we return true or false accordingly:
extension ShoppingListViewController: EditItemViewControllerDelegate {
func isItemPresent(item: String) -> Bool {
if let _ = shoppingList.items.firstIndex(of: item) {
return true
} else {
return false
}
}
// Rest content...
}
The information we need can be now given from ShoppingListViewController
to EditItemViewController
when required. However we still have to update our previous saving code in EditItemViewController
so we can actually make use of it.
Let’s pay a second visit to the saveItem(_:)
IBAction method in the EditItemViewController.swift file. Our update will make the shouldAdd(item:)
delegate function get called if only the isItemPresent(item:)
returns false. Otherwise… let’s be polite! We’ll present an alert notifying users that the current item has been added already to the shopping list.
Replace the previous contents of the saveItem(_:)
IBAction method with these shown below:
@IBAction func saveItem(_ sender: Any) {
guard let text = textField.text else { return }
if text != "" {
if let delegate = delegate {
if !delegate.isItemPresent(item: text) {
// Item doesn't exist in the items collection,
// so let's add it now.
delegate.shouldAdd(item: text)
navigationController?.popViewController(animated: true)
} else {
// Item exists already in the items collection.
// Show an alert to indicate that.
let alert = UIAlertController(title: "Item exists", message: "\(text) already exists in your shopping list.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
}
}
Running the app again will show that this new addition we just made is the solution we have been looking for!
More Delegate Functions
Even though we have gone from nothing to something, our demo application would be more meaningful and complete if we support two more features:
- Edit and replace an existing item,
- Remove an item.
Both of these actions are initiated in EditItemViewController
, but they actually regard the ShoppingListViewController
class. So, since EditItemViewController
has to say something to ShoppingListViewController
again, we are in the need of two new delegate functions.
We’ll begin by implementing the edit and replace functionality. What we need to achieve here is to present the EditItemViewController
and pass an existing item to it, so users can update it and save it back. Which one? Any item tapped on the table view in the ShoppingListViewController
view controller.
For our convenience, the way to select an item and pass it to the EditItemViewController
has been implemented in the starter project already. Any selected item in the table view in the ShoppingListViewController
is assigned to the editedItem
property of the EditItemViewController
, so we take it for granted that the editedItem
is the one that holds the item that should be edited when tapping on any item in our list.
Focusing again on the delegate functions we want to add, let’s head back to the EditItemViewControllerDelegate
protocol, which we extend with the following function:
protocol EditItemViewControllerDelegate {
...
...
func shouldReplace(item: String, withItem newItem: String)
}
What that method is supposed to do is shown pretty clearly in its implementation that takes place in the ShoppingListViewController.swift file right next:
extension ShoppingListViewController: EditItemViewControllerDelegate {
// Previous function definitions...
func shouldReplace(item: String, withItem newItem: String) {
if let index = shoppingList.items.firstIndex(of: item) {
shoppingList.items[index] = newItem
tableView.reloadData()
}
}
}
If the item is found, then we replace it with the new value and the table view is reloaded, otherwise nothing happens.
We’ll go now to the saveItem(_:)
IBAction method in the EditItemViewController
for a third time, as we need to update it even further.
We’ll focus on the case where the item we’re trying to save is not present in the items collection, and we’ll check to see if we’re editing an item or not. We can achieve that simply by checking if the editedItem
value is nil or not. If it’s not nil and therefore we’re editing an existing item, we’ll call the brand new delegate function we implemented in this part. Otherwise, we’ll save the item as a new one.
@IBAction func saveItem(_ sender: Any) {
guard let text = textField.text else { return }
if text != "" {
if let delegate = delegate {
if !delegate.isItemPresent(item: text) {
// This is the point of interest.
// If the editedItem is not nil, then an item is being edited.
if let editedItem = editedItem {
delegate.shouldReplace(item: editedItem, withItem: text)
} else {
delegate.shouldAdd(item: text)
}
navigationController?.popViewController(animated: true)
} else {
let alert = UIAlertController(title: "Item exists", message: "\(text) already exists in your shopping list.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
}
}
Let’s make it possible to delete an item now. For that, we’ll add one last delegate function in the EditItemViewControllerDelegate
protocol:
protocol EditItemViewControllerDelegate {
// Previous delegate functions...
func shouldRemove(item: String)
}
Its implementation in the ShoppingListViewController
view controller is more than straightforward. Open the ShoppingListViewController.swift file and update it with the following:
extension ShoppingListViewController: EditItemViewControllerDelegate {
// Previous function definitions...
func shouldRemove(item: String) {
if let index = shoppingList.items.firstIndex(of: item) {
shoppingList.items.remove(at: index)
tableView.reloadData()
}
}
}
Back to EditItemViewController
you’ll find an IBAction method called deleteItem(_:)
which is still empty. Time to make it working:
@IBAction func deleteItem(_ sender: Any) {
guard let text = textField.text else { return }
if let delegate = delegate {
delegate.shouldRemove(item: text)
navigationController?.popViewController(animated: true)
}
}
Test the app again. Now it’s totally possible to add, edit and remove items in the shopping list!
Notifications
Employing iOS SDK’s NotificationCenter
class to send notifications is a great way to exchange messages internally in your app. Notifications may contain or not data, and it’s a good way to support communication among various parts of the app that are either directly related or not.
The particularly interesting thing with notifications is that the sender (the creator of the notification) broadcasts a message, and anyone observing for it can get it and perform its own actions. Even though we won’t take advantage of that feature of notifications here (we’ll stick to one-to-one communication, not one-to-many), you should be aware of it in case you need it.
Implementing notifications on an iOS (or any other Swift based) project has a quite particular workflow:
- Post the notification. That happens on the sender’s side, and it’s the place where we include any additional data that might be required by the receivers of the notification.
- Observe for the notification on any class or struct that required.
- Implement a function that defines all the actions that should take place when the notification is received.
Custom names must be given to custom notifications, and naming is quite important, because it makes it easy to distinguish notifications. Keep that in mind, as having multiple custom notifications in large projects can become really confusing, and good naming can save you time from searching around what each notification is for. More about that is coming right next.
Implementing Notifications On The Demo project
Back to our demo project. So far, we are able to add items to a shopping list, however we don’t keep that list into the collection of lists in the AllListsViewController
. What we are obviously missing is to make the ShoppingListViewController
class capable of saying to AllListsViewController
that a new list has been created. Based on what we’ve seen so far, we could use delegation pattern to achieve that, but no! This time we’ll do it with notifications!
For the purpose of this tutorial, we are going to create two custom notifications:
- The first will notify
AllListsViewController
that a new shopping list has been created. - The second will notify
AllListsViewController
that an existing shopping list has been modified.
The sender of both notifications will be the ShoppingListViewController
class, as that’s the one knowing when to post them. AllListsViewController
will observe for them, and it will perform certain actions upon receiving them.
Specifying Notification Names
Before we start implementing, let’s focus on the notification names first. A notification name is of Notification.Name
type, where Name
is a struct in the Notification
class (find out more here).
Extending Notification.Name
struct with custom notification names is considered as a best practice so they’re widely available all over the project. That way not only is unnecessary to remember them, but it also helps to avoid typographic mistakes. Plus, Xcode will auto-suggest them!
So, this is what we’re about to keep on doing on our demo project. In the starter project, there is a file called NotificationNameExtension.swift. Meanwhile, the is blank. Let’s change that by extending the Notification.Name
struct and create our custom notification names:
extension Notification.Name {
static let didCreateShoppingList = Notification.Name("didCreateShoppingList")
static let didUpdateShoppingList = Notification.Name("didUpdateShoppingList")
}
Both names describe what they are all about. They are declared as static properties, so they become part of the type and we don’t need to create instances of Notification.Name
so we can use them.
Posting a Notification
Now that our custom notification names exist, let’s post our first notification. Head over to the ShoppingListViewController.swift file, and look into the updateParent()
method. This method is called every time the back bar button is tapped, so it’s the best place to let AllListsViewController
know that we have finished creating or editing a shopping list.
The most obvious way to send a notification is to initialize a Notification
object and then post it. Notification name and any data can be given upon initialization. Here’s how to initialize a notification without data:
let notification = Notification(name: .didCreateShoppingList)
And here’s how to initialize a notification with data:
let notification = Notification(name: .didCreateShoppingList, object: AnOptionalObject, userInfo: AnOptionalDictionary)
object
expects for a single value of Any
type and it can be nil
.
userInfo
is an optional value too and it expects for a dictionary into which you can have as much data as you want.
To send the notification, simply call:
NotificationCenter.default.post(notification)
All the above is an unnecessary effort as notifications can be created and sent in one line only. Simply use any of the following:
post(name:object:)
: It creates and posts a notification with an optional object (Any
object). Keep itnil
if you don’t want to post any data along with the notification.post(name:object:userInfo:)
: It creates and posts a notification with an optional object or a dictionary. It’s similar to the equivalent notification initializer. Passnil
as an argument to any parameter (object
oruserInfo
) you don’t want to send data for.
The last one is what I always prefer to use, as it’s the most flexible option and easy to change during development. Let’s modify the updateParent()
method as follows:
func updateParent() {
if shoppingList.items.count > 0 {
NotificationCenter.default.post(name: .didCreateShoppingList,
object: shoppingList.items,
userInfo: nil)
}
}
The above will post the .didCreateShoppingList
(Notification.Name.didCreateShoppingList
) notification and it will include the items collection as the notification’s object every time we tap on the back button in the ShoppingListViewController
. We set nil
to the userInfo
dictionary, there’s nothing more to post.
Handling the Notification
Let’s switch to the AllListsViewController.swift file, and let’s start by observing for the above notification. Go to the viewDidLoad()
method and add the following line:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(handleDidCreateShoppingList(notification:)), name: .didCreateShoppingList, object: nil)
}
With that line, we are adding our class as an observer of the didCreateShoppingList
notification to the Notification Center, and we’re telling our app to call the handleDidCreateShoppingList(notification:)
method upon receiving it. This method doesn’t exist yet, so let’s go straight ahead to create it.
Still in the AllListsViewController
class, add the following:
@objc func handleDidCreateShoppingList(notification: Notification) {
if let items = notification.object as? [String] {
let newShoppingList = ShoppingList(id: listManager.getNextListID(),
name: "Shopping List",
editTimestamp: Date.timeIntervalSinceReferenceDate,
items: items)
listManager.add(list: newShoppingList)
tableView.reloadData()
}
}
The first thing we do here is to make sure that the object
property of the notification has an actual value. Always do that check and never use the object
property directly if you want to be safe and away from app crashes. At the same time, and if object
is not nil
, we cast it to an array of String objects, as the didCreateShoppingList
notification brings along the collection of items added to the shopping list.
The rest of the code is specific to the demo app we’re building. We create a new ShoppingList
object that will get the proper ID, the default list name, the current timestamp and the items collection. That object is added to the lists
collection of the listManager
object in the AllListsViewController
view controller, and finally the tableview is reloaded so we visually reflect any changes made.
If we run the app at this point and we create a new shopping list, it will be listed in the AllListsViewController
view controller.
Completing Implementation With A New Notification
Now that we made it possible to save and show a new shopping list in the AllListsViewController
view controller, let’s keep on working on the same logic and create a new notification that will enable us to update shopping lists properly. By testing our demo app on its current state, we can see that it’s possible to edit an existing shopping list, however every time we go back to the initial screen the list is not updated; a new entry is being created instead.
That behaviour is pretty much expected, as the only thing that happens when we tap on the back button in the ShoppingListViewController
class is the notification posting that “says” to AllListsViewController
to create a new shopping list. So, it’s necessary to go back to ShoppingListViewController.swift file and revisit the updateParent()
method to change that.
We will update that method based on a single fact:
When a new shopping list is being created, the shoppingList
property in ShoppingListViewController
has no id
value (it is nil
). On the contrary, when a shopping list is being edited the id
property of the shoppingList
object has a value and it is not nil
.
You can verify that in the prepare(for:sender:)
method in the AllListsViewController
class, where if a list has been selected (selectedListIndex
is other than nil
), the id of the shopping list is passed along with other values to the shoppingList
property of the ShoppingListViewController
:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let identifier = segue.identifier {
if identifier == "idEditShoppingListSegue" {
if let shoppingListVC = segue.destination as? ShoppingListViewController {
if let index = selectedListIndex {
shoppingListVC.shoppingList.id = listManager.lists[index].id
shoppingListVC.shoppingList.name = listManager.lists[index].name
shoppingListVC.shoppingList.items = listManager.lists[index].items
}
}
}
}
}
So, now that we know that, it’s time to change the updateParent()
method as shown next:
func updateParent() {
if let id = shoppingList.id {
NotificationCenter.default.post(name: .didUpdateShoppingList, object: nil, userInfo: ["id": id, "items": shoppingList.items])
} else {
if shoppingList.items.count > 0 {
NotificationCenter.default.post(name: .didCreateShoppingList, object: shoppingList.items, userInfo: nil)
}
}
}
In human words, if the shopping list has an id value, then that means that we are updating it and we are posting a brand new notification (didUpdateShoppingList
). Otherwise, we are talking about a shopping list that we just created and the notification we sent in the previous part will be posted. Pay attention to that new notification:
NotificationCenter.default.post(name: .didUpdateShoppingList, object: nil, userInfo: ["id": id, "items": shoppingList.items])
Two noticable things here:
- The first is the notification name, which is obviously different than the first one.
- The second is that we are not using the
object
parameter value here. Instead, we are making use of theuserInfo
dictionary and we pass two different kind of data: The id of the edited shopping list, and the items collection.
In the AllListsViewController.swift file now, let’s update the viewDidLoad()
method so we make it possible to observe for that new notification:
override func viewDidLoad() {
// ...
NotificationCenter.default.addObserver(self, selector: #selector(handleDidUpdateShoppingList(notification:)), name: .didUpdateShoppingList, object: nil)
}
This time, we call the handleDidUpdateShoppingList(notification:)
method when the didUpdateShoppingList
notification gets received. Our next task is to implement it:
@objc func handleDidUpdateShoppingList(notification: Notification) {
if let userInfo = notification.userInfo {
if let id = userInfo["id"] as? Int, let items = userInfo["items"] as? [String] {
listManager.updateItems(inListWithID: id, items: items)
tableView.reloadData()
}
}
}
Note that it’s necessary to make sure that the userInfo
dictionary is not nil
before proceeding. After that, we just update the list in the shopping lists collection of the listManager
, and we reload the tableview.
Build and run the app again. This time you’ll see that when you edit a shopping list and then go back to the first view controller, it is properly shown in the tableview. Moreover, the last updated list is shown to the top.
Before continuing to the next part, one last thing: Stop your class from observing for notifications when it’s about to be destructed (deallocated). In the demo app where we observe for notifications in a view controller, here’s how we do that:
deinit {
NotificationCenter.default.removeObserver(self)
}
Add the above to the AllListsViewController
class right after the viewWillAppear(_:)
method.
Closures
Apart from delegation pattern and notifications, there is one more straightforward, less used probably, method to send messages among classes in an app: Closures and action handlers. Most probably, the nature of writing closures is what it makes it less attractive. That shouldn’t be happening though; the powerfulness of this method can be seen once someone understands how to write them.
To have a first understanding about how this approach works, think again of the two hypothetical classes we talked about earlier, ClassA
and ClassB
, where ClassB
is the one that needs a way to talk back to ClassA
.
In its simplest form, ClassB
implements a method where at least one parameter is a closure:
func myMethod(handler: @escaping () -> Void) {
}
Think of it like that: You pass a function as the parameter of another function. In this example, handler: () -> Void
is the function parameter. In this case it has no arguments, and it’s a void function, something that must be explicitly declared.
Why @escaping
? Shortly, so we can access the action handler (the closure) after the execution of the method has finished. Read more here.
Next, myMethod(handler:)
keeps a reference to the the handler
parameter in a class property, so it can be called when it’s necessary for ClassB
to communicate with ClassA
.
This can happen if only the class property’s type is the exact handler declaration:
class ClassB {
var actionHandler: (() -> Void)?
}
It’s an optional value, as by default it points to nothing.
Here is how myMethod(handler:)
keeps the reference to the handler function:
class ClassB {
// ...
func myMethod(handler: @escaping () -> Void) {
actionHandler = handler
}
}
Later on, ClassB
calls the action handler when it’s about time to communicate with ClassA
. The only almost mandatory thing is to make sure that the action handler property is not nil
:
class ClassB {
// ...
// A hypothetical method that processes data and then
// notifies ClassA that its work is complete.
func process() {
// process data
if let actionHandler = actionHandler {
actionHandler()
}
}
}
One the other side now, when ClassA
calls myMethod(handler:)
through the initialized ClassB
object, it implements a closure, meaning a block of code that it won’t be executed unless the handler function is called in ClassB
:
var classBObject = ClassB()
classBObject.myFunction {
// This is the closure. This is also the place where ClassA implements
// any code that should be executed when the action handler is called
// in ClassB. When this block of code is executed, then ClassB has
// successfully "talked back" to ClassA.
}
The above could be written also as:
classBObject.myFunction(handler: { () in
})
Data can be passed from ClassB
to ClassA
, as long as the matching parameter values are declared in the handler functions. For example:
func myMethod(handler: @escaping(_ success: Bool) -> Void) {
}
Calling it:
classBObject.myFunction(handler: { (success) in
if success {
// Do this...
} else {
// Do that...
}
})
The purpose here is to get the big picture, so don’t worry about the details or if you don’t fully get it. We are going to see specific examples right next. Before we get there though, here’s a popular iOS SDK’s method that uses action handlers:
UIView.animate(withDuration: 0.4, animations: {
self.renameListView?.alpha = 1.0
}) { (finished) in
if finished {
self.renameListView?.textField.becomeFirstResponder()
}
}
The above is one of the simplest ways to perform animations on iOS, and there are serious chances that you have seen something similar or written animations already, so most probably it looks familiar. The specific code above is taken straight from the demo app, and it can be found in the AllListsViewController
, showRenameListView()
method.
See that there are two handlers here: One that is called to perform the actual changes in the UI that produce the desired animation (animations
closure), and one to notify that animation is finished (completion closure).
An action handler being last in a method or function is usually called completion handler because most of the times it signals the end of some process.
Before we focus again on the demo app, you are encouraged to make a search on the Internet about closures, action and completion handlers so you have a better understanding of everything discussed here.
Action Handlers In… Action
After that introductory discussion above, let’s become specific again and let’s talk about our demo app. Our goal in this part is to enable renaming of shopping lists.
In the starter project, a swipe-left gesture on a list in AllListsViewController
reveals two options that allow to rename and delete the selected shopping list. So far, a custom view called RenameListView
shows up when tapping on the Rename context button of any shopping list cell, but none of the Rename or Cancel buttons in it work. And our mission is to change that.
We will start working in the RenameListView.swift file. As said, RenameListView
is a custom view implementation that we will use to rename any shopping list.
As a first step, let’s make our cancel button work. In the beginning of the RenameListView
class, add the following declaration:
var cancelHandler: (() -> Void)?
The above property will keep the reference to the actual action handler, which is also the one and only parameter of the following method (add it to the RenameListView
class too):
func handleCancelRenaming(handler: @escaping () -> Void) {
cancelHandler = handler
}
The handleCancelRenaming(handler:)
method will be called through the AllListsViewController
class in a while. Here we just define it, but it’s crucial to keep the action handler out of the scope of that method so we can access it later on. That’s why we assign it to the cancelHandler
property.
Note: Consider the above two steps as a standard procedure when you implement action handlers as a communication solution among classes. Of course, naming won’t be the same, as well as any potential parameters in action handlers. Despite that, logic remains always the same!
The third and last step here is to call the cancelHandler
handler when the Cancel button is tapped. Go to the cancel(_:)
IBAction method that has been defined already in the starter project, and update it as shown next:
@IBAction func cancel(_ sender: Any) {
if let handler = cancelHandler {
handler()
}
}
Since cancelHandler
was originally declared as an optional, it’s really vital to make sure that it is not nil
.
What we did so far doesn’t mean that the RenameListView
will actually disappear; there is no code for that! All we did was to make it possible to call an action handler when the Cancel button gets tapped. We are still missing the implementation of that action handler.
Open the AllListsViewController.swift file, and go to showRenameListView()
method. There is already some code in the body of that method, and it’s the place where renameListView
, a RenameListView
class object is being initialized and makes the respective view appear when the Rename context button is tapped on a cell.
At some point in that method you’ll find a comment saying: “// Add action handlers here!”. Go a couple of lines after it, and call the handleCancelRenaming(handler:)
method through the renameListView
object. While you’ll be typing, Xcode will auto-suggest it. Just hit Return to get the following:
renameListView?.handleCancelRenaming {
}
And our closure is there! Let’s make renameListView
disappear:
renameListView?.handleCancelRenaming {
self.renameListView?.removeFromSuperview()
self.renameListView = nil
}
The Cancel button in the rename view is fully working now! And if you think about it, RenameListView
class says to AllListsViewController
when to make it disappear, so our goal has been achieved!
We are not done yet though. Let’s follow the same process, and let’s enable renaming. The steps needed for that are pretty much the same as above. The only difference here is that we’ll see how to carry data around: The new shopping list name.
Back to RenameListView.swift file, let’s start by declaring the next property that will keep a reference to the rename action handler:
var renameHandler: ((_ name: String) -> Void)?
Next, let’s implement the following method and give AllListsViewController
class the option to know when renaming takes place:
func handleRenaming(handler: @escaping (_ name: String) -> Void) {
renameHandler = handler
}
Watch the new thing here: There is a parameter in the handler method.
In the rename(_:)
IBAction method we will call the renameHandler
. Before we do so, we must make sure that user has typed some text. Here it is all together:
@IBAction func rename(_ sender: Any) {
guard let text = textField.text else { return }
if text != "" {
if let handler = renameHandler {
handler(text)
}
}
}
Notice that the text
value is given as an argument to the action handler, which is expecting for it since we declared it that way. It is not allowed to call the handler without any arguments, or with more arguments than the expected one. You should always pass as many arguments as the parameter values in the handler declaration are!
Our work in the RenameListView
class is done. Let’s pay a final visit to AllListsViewController.swift file, and in the showRenameListView()
method again. Right below the implementation of the previous action handler, add the following:
renameListView?.handleRenaming(handler: { (listName) in
if let index = self.selectedListIndex {
self.listManager.lists[index].name = listName
self.tableView.reloadData()
self.renameListView?.removeFromSuperview()
self.renameListView = nil
}
})
In the above snippet, we first make sure that the index to the edited list exists, and then we access that list through the listManager
property and we update its name. Of course, we also reload the table view so the new name gets displayed. But besides all that, the most important thing is the way closure is written when there’s a parameter (or more) in the handler, and how that parameter is used.
That’s it, try the app again now and rename your shopping lists!
Conclusion
Our journey on how to implement communication internally in apps has come to its end. After you comprehend what you read here, most probably you will start wondering which method to choose when it’s about time to use them in your own projects. The answer is that there are no clear rules, and you choose what fits best to your app. As a rule of thumb, the order in which the three different approaches were presented here should also be the order of your preference.
Delegation can be used pretty much everywhere, but notifications could be proved a simpler and faster implementation in certain circumstances, for example when data fetching from a server is finished and you want to notify the view controller that is going to display it. Closures are powerful too and ideal for keeping everything in one place, or when implementing delegation or notifications seem to be an overkill for small objectives.
Undoubtedly, experience will be your guide, so the more you use them and the more use cases you meet, the easier for you to decide. I hope you enjoyed this post, and if so, share it! Thanks for reading, see ya!
For reference, you can download the complete project on GitHub.