Newbies’ Guide To iOS Callbacks
Bài đăng này đã không được cập nhật trong 8 năm
Unlike my other posts, this time I’m writing a tutorial for the beginners. As you can tell from the title, this time it’s about iOS, more specifically its callbacks.
Since this post is aimed for the newcomers in iOS (or any mobile platform) development, I think it’s necessary to discuss a few things in some detail before coming to our main topic.
In application development, there comes situation like one class calling a method of some another class to do something. Upon completing that task in the called method, the caller class decides what to do next. In this kind of scenario the caller class needs to know exactly when the called method finishes its job or maybe the caller class must need some data to execute the next lines of codes, but the data must be served from the called method after it gets the job done.
So, what’s the big deal here? Upon returning from a method, the caller always gets its data. There’s nothing to worry about here. Or is it? Let’s find out.
In case of multithreading, where it is a good practice to hand the memory and time-intensive tasks over other threads, or in case you like to do something in the background while leaving the UI thread free to user interaction and do other business in the meantime, you must make sure that the UI thread (or any other thread you like) knows when the background tasks are finished so that you can update the UI as per the data you get from the background. In this case, you need to come to an agreement with the method that performs background task to make sure it delivers you the data you need when it finishes its job. To do this, you have to create a medium of communication between classes, and here come callbacks to lend you the helping hand.
Let’s imagine a scenario where you want to click a button, say ‘OK’ button of an alert dialog. This click is an event that you want the system to know about. Just think of it as a ‘call’ to the system. When the system gets to know about this, it calls you back with some necessary information needed to perform a task, say dismiss the view controller. You need a method/function for this to accomplish which may take one or more parameters (or no parameters at all) to serve the necessary information to you. This is called the callback method/function/block and in iOS’ terms, takes the form of a closure. Inside that block you write your code to perform the desired actions.
This is the most basic idea of callbacks. There’s no better way to explain this than writing some code to create an app that relies on some sort of communication between two of its classes. We’ll come to that very soon, but, before that, let’s see how many ways there are to accomplish the task of back-and-forth communication. For the simplicity of our example, our app will demonstrate the following techniques to show you the basics.
- The delegation pattern: if you ever played with Android, then this pattern should be familiar to you. It’s exactly like implementing an interface in a class and setting the listener of your objects with the implementation of that interface. In iOS, the term ‘interface’ is replaceable with ‘protocol’ and ‘listener’ with ‘delegate’. It may sound a bit messy and scary at this moment, but I’ll explain all these through the example app.
- The observer pattern: it is basically a publisher-subscriber (pub-sub) pattern. There can be multiple classes subscribing to a class, just like we tune our radios to a specific station. When the class publishes something, like the station broadcasts, the subscriber classes get to know about it. This pattern is useful in cases where multiple classes depend on one class to perform something.
- Callbacks using Closure: this is like magic. Closure is just a function without a name. It can have its own parameters and return statement, and can be passed as an argument to another method. It is easier to write (and read) than implementing the delegation or observer pattern. This is suitable for one-to-one communication between classes.
Enough talking, let’s do some real business to make everything clear.
In our example app, there are two view controllers. In the first one, user has to provide the values of two operands and choose one operation from summation, multiplication and concatenation. User can also select the datatype of those operands. Clicking the button denoting the operation type, user can go to the second view controller where he/she can see the result along with the numbers of each data type the operations have been performed on. Upon going back to the first view controller, user can see the updated number of each operations performed. To update the numbers of operations in the first view controller, I’ve used the three patterns stated above, each manipulated using their respective buttons. Have a look below to get an idea of what the app looks like.
Before diving deeper into the codes, I would recommend to download the project from GitHub and build in your Xcode. It’s written in Swift 2.0 and my Xcode version is 7.3.1. This will save a lot of time for both you and me from building the entire app from the scratch and me telling you every step down the way. And don’t forget to run the app on simulator. Also keep in mind that I won’t explain the whole project line by line since it will be very tiresome not only for you, but also for me. So, I’ll skip a lot of details to make this post as compact as possible to make sure we’re not deviating from the main topic which is to show you how to communicate with other classes and how easy it can get using callbacks.
The project starts with creating the storyboard. As you can see from the downloaded project, there’s Main.storyboard containing two scene’s for the two screens of our app. Likewise, there are HomeViewController.swift
and ResultViewController.swift
for the 1st and 2nd scenes respectively to handle all the user interactions and logics.
In the first scene, there are three buttons on the top to choose the data type. Below that, two TextFields along with a Label for each to get the input of X and Y values. At the bottom, there are three more buttons to select the operations. After selecting the data type and providing the inputs, user can click one of the buttons at the bottom to see the result in the next screen. To prevent user from making blunders, we want to disable the ‘Concat’ button if ‘Int’ or ‘Float’ buttons are clicked.
Similarly clicking the ‘String’ button will disable the ‘Sum’ and ‘Mul’ buttons. Upon returning from the second screen the three Labels at the bottom will bIn the first scene, there are three buttons on the top to choose the data type. Below that, two TextFields along with a Label for each to get the input of X and Y values. At the bottom, there are three more buttons to select the operations. After selecting the data type and providing the inputs, user can click one of the buttons at the bottom to see the result in the next screen. To prevent user from making blunders, we want to disable the ‘Concat’ button if ‘Int’ or ‘Float’ buttons are clicked. The three Labels below them will be updated with the number of operations. I’m going to leave the codes to you to understand it by yourself (if you wish ). You can have a look at the HomeViewController.swift file up here. For now, I’m going to stick to the main topic of this post.
As I’ve already said what the buttons for summation, multiplication and concatenation operations calculate the result and pass it to the next view controller, there’s a method named ‘calculate’ to perform all these tasks. You just have to tell it what operation it should perform by passing the operation name as its argument. You can forget everything and focus just on this method which starts right here.
func calculate(operation: String) {
...
...
}
The operations are defined in the Operations structure in the Constants.swift file. After calculation, it passes the result to the second view controller and right at that moment lies all this post is about. But we will come to that later. Now let’s see what the other scene of our storyboard has in it for us.
In the second scene of our storyboard, there are four Labels we have to worry about. The first is to display the value of the result passed from the first view controller. In the middle, there’s a TableView that shows all the results previously calculated. At the bottom, the three other Labels are to show the number of data types the operations have been performed on. Below that, there are three buttons ‘Delegate’, ‘Observer’ and ‘Closure’. All of these three buttons are used for returning to the first screen, and here goes the demonstration of how the delegation, observer and callback patterns work.
In the project, I saved the data in the UserDefaults so that after closing and reopening the app, all the data persists. But, for the sake of the simplicity of this tutorial and sticking to the main topic I would like to skip that part along with the TableView. So, please pardon me for that.
Now let’s have a look at the ResultViewController.swift. First we’ll see how delegation pattern works. For that, we need a protocol
defined before the class starts. I’ve named it ResultViewControllerDelegate
and it has a method named onDataSaved
with the parameter data
which is an array of Result
, a model created in the file Result.swift.
protocol ResultViewControllerDelegate {
func onDataSaved(data: [Result])
}
Inside the class, I’ve declared an instance of ResultViewControllerDelegate
named delegate
.
...
var delegate: ResultViewControllerDelegate?
...
Now let’s go back to the HomeViewController
. For delegation to work, this class needs to conform to the ResultViewControllerDelegate
protocol.
class HomeViewController: UIViewController, ResultViewControllerDelegate {
...
}
When it conforms to the protocol, it needs to override the delegate methods in it. It’s written at the very bottom of the class like this:
class HomeViewController: UIViewController, ResultViewControllerDelegate {
...
//MARK: - ResultViewControllerDelegate
func onDataSaved(data: [Result]) {
updateLabels(data)
}
}
Now look at the calculate
method. After calculation is done and secondViewController object is initialized as the ResultViewController
using the identifier, the third line assigns the conforming HomeViewController
to the delegate
variable of the ResultViewController
.
func calculate(operation: String) {
...
let secondViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ResultViewController") as? ResultViewController
secondViewController?.result = Result(argument1: valueOfX.text!, argument2: valueOfY.text!, operation: operation, result: result, dataType: dataType)
secondViewController?.delegate = self
...
}
Now, when the Delegate button of the ResultViewController
is clicked, inside the action method the delegate
object’s onDataSaved
method is called while passing the results
array to it and then dismisses the ResultViewController
.
@IBAction func delegate(sender: UIButton) {
...
delegate?.onDataSaved(results)
self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
What happens here is that, the onDataSaved
method of the HomeViewController
is getting called back with the results
array as its argument. Now it can do whatever you want with the data it gets from the ResultViewController
. In our app, we update the Labels with the number of operations.
//MARK: - ResultViewControllerDelegate
func onDataSaved(data: [Result]) {
updateLabels(data)
}
That’s it. Whenever you click the Delegate button of the ResultViewController
, the data is passed on to the onDataSaved
method of the HomeViewController
. That’s how the delegation pattern works.
Now let’s see how the observer pattern works. For that, we need a publisher class which in our case is the ResultViewController
. When the Observer button is clicked, the following action method is invoked.
@IBAction func postNotification(sender: UIButton) {
Result.saveResults(results)
NSNotificationCenter.defaultCenter()
.postNotificationName(NotificationKeys.notificationKey,
object: nil,
userInfo: ["message": results, "date": NSDate()])
self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
Here you can see, we used NSNotificationCenter
API to post notification with the a key and a dictionary as userInfo
which contains our results
array as its message
which is all the subscriber classes, which in our app is only the HomeViewController
, need to know.
To receive the notification in the HomeViewController
, we write this in the viewDidLoa
d method which receives the the notification with the same key that the ResultViewController
used to post:
NSNotificationCenter.defaultCenter()
.addObserver(self,
selector: #selector(self.catchNotification(_:)),
name: NotificationKeys.notificationKey,
object: nil)
And in the catchNotification
method, we get our desired data embedded in the notification! And that's how the observer pattern ends right here.
Now let’s face the callback pattern. All you have to do is declare a closure in the ResultViewController
like the following:
...
var onClosureButtonTapped: ((data: [Result]) -> Void)? = nil
...
Now in the calculate
method of the HomeViewController
, let’s define what the closure will do when it gets its data right before the second view controller is presented.
secondViewController?.onClosureButtonTapped = {(data: [Result]) -> Void in
self.updateLabels(data)
}
What it actually does is assign the closure to the onClosureButtonTapped
declared in the ResultViewController
.
In the ResultViewController
, the action method for the Closure button looks like this -
@IBAction func closure(sender: UIButton) {
Result.saveResults(results)
if let onClosureButtonTapped = self.onClosureButtonTapped {
onClosureButtonTapped(data: results)
self.presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
}
}
To put it simply, after saving the results
, it passes the array to the onClosureButonTapped
closure as the argument and just like that, it calls the HomeViewController
back with the data it needs. As we’ve already defined in the claculate
method of our HomeViewController
what the closure will do once it gets the data, it will update the Labels like a charm.
That’s how simply you can use a closure as a callback to handle response from other classes. As you can see, it is very much like the delegation pattern, but requires a lot less code to get the job done. You can use closure as callbacks for any method you wish without the hassle of creating and conforming to protocols, setting delegate and overriding the delegate methods. For example look at the code below to show an alert dialog that has an OK button. We can tell the alert dialog what the OK button should do using a closure as the button’s callback:
let okCompletion: () -> () = {
print("OK button pressed")
}
let alert = UIAlertController(title: "Alert", message: "Message", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: {UIAlertController in
okCompletion()
}))
self.presentViewController(alert, animated: true, completion: nil)
That's all for today. If all these seem a bit difficult to grasp, don't worry. As the saying goes, practice makes perfect. So, happy coding.
All rights reserved