Very regularly when dealing with asynchronous stuff (among other things) in iOS development, we have to make a decision between three tools.
- Closures (blocks, callbacks, etc)
NSNotificationCenter
- The delegate pattern.
Which one is right for you depends on the scenario. It won't always be the same one. As a general rule, we can use the number of objects we want to respond to the result as a guideline for which pattern to use.
- Closures (blocks, callbacks, etc.) are good for when we want zero objects tied to waiting on the result. We just pass the closure and forget about it. 0 objects.
NSNotificationCenter
is a good approach when we want any number of unknown objects to be able to respond to the result. The thing working on producing the result knows nothing about these objects, it just creates a notification when it's complete. 0-many objects.
- The delegate pattern is good when we want exactly one object to respond to the result. Using the delegate pattern, the delegate and the worker object know quite a bit about each other. The worker object will define a protocol of set methods that its delegate must implement or can optionally implement, and the worker object can call any of these. 1 object.
It looks here like we most likely want the delegate pattern for our scenario, given that our generateRefreshToken
method takes a custom notification string. But I will cover all three options lightly (and perhaps after picking one you for sure want to use, you can improve your code and post another question where we cover that topic in detail).
For all of these scenarios, we need to consider the same information--what do we need to pass to our completion handling code?
- Some indication of success vs failure.
- If failure, details on the failure.
- If success, the results.
Closures (blocks, callbacks, etc)
This is basically the approach that joekim's answer takes. Our method, generateRefreshToken
just needs to take a single argument, a completion block. That completion block should be defined as a closure that accepts arguments that convey the information we outlined above, and return void. joekim's answer outlines a very good approach to this, but how you implement it specifically is up to you. Frequently these sorts of closures will simply take an NSData
or NSDictionary
object to represent the data (or perhaps just a regular Swift Dictionary
) and an NSError
optional to indicate success or failure (and hold failure details).
NSNotificationCenter
This is effectively the pattern you're currently using. But remember, I described NSNotificationCenter
as being for when you want zero to many objects responding to completion. As such, when we use NSNotificationCenter
, we probably should be accepting a string parameter to tell us the name of the notification. Instead, the file that declares this class should probably also expose two String
constants that the object will use for its notification. One of these notifications is the "success" notification, and the other is the "error" notification. The "userInfo" passed with the "success" notification is simply the necessary data we want to collect, and the "userInfo" passed with the "failure" notification simply contains the error.
The reason that I recommend this approach (two notifications) is simply because NSNotificationCenter
allows observers a lot of control and flexibility.
If I want to use a single method to observe both methods and just check to see what's in user info, I can.
If I want to use two methods and separate my error handling logic from my success handling logic, I can.
If I want to ignore failure (or success), I can. If I want to have one object handle success and another handle failure, I can.
The Delegate Pattern
This is the pattern I actually think is most appropriate for your scenario. For this pattern, we must start by declaring a protocol
that defines the methods we'll be calling on our delegate. Something like this might work:
@objc protocol GenerateTokenDelegate {
func tokenGeneratorDidSucceed(tokenGenerator: DeviceInfo)
func tokenGeneratorDidFail(tokenGenerator: DeviceInfo)
}
Of course, this approach means we must declare our protocol as @objc
, and our class DeviceInfo
, must also be an Objective-C class (make it inherit from NSObject
), and we must do this so we can hold a weak reference to our delegate. If we hold a strong reference to our delegate (which will typically already have a strong reference to us), we risk creating a retain cycle. The delegated object should hold a weak reference to its delegate.
So now we need to add a weak reference to our delegate to our class:
class DeviceInfo:NSObject{
weak var delegate: GenerateTokenDelegate?
// the rest of our class definition
}
Now, when we instantiate our class, we set its delegate
property equal to the object we want it to call back to (which conforms to GenerateTokenDelegate
protocol), and when we've completed our asynchronous code, we call back to it as such:
self.delegate?.tokenGeneratorDidFail(self)
And obviously, we can set our protocol up to define our methods in any such way and take any set of arguments (though we'll always want to pass self
as the first argument, typically).
We can also declare optional methods. Suppose we maybe don't want to force the delegate to handle errors. We can let the "didFail" method be an optional part of the protocol.
optional func tokenGeneratorDidFail(tokenGenerator: DeviceInfo)
And now we'd call it as such:
self.delegate?.tokenGeneratorDidFail?(self)
Notice both sets of question marks. The delegate
may or may not be nil
, and even if it's not nil
, it may or may not implement the method we're trying to call.