此文章同时提供中文版本:TinyExtension:空Closure简化
In Swift, we often use Closure. Of course, most of Closure is used to implement specific function blocks, but in some cases we will use empty Closure:
Case 1: Ignore callback
Normally the callback method will be implemented, but sometimes the interface provides more functions than we need. For example, we need to use the following function:
func sampleRequest(success: (Bool, String) -> (), failure: () -> ()) {
// some network request logic
// with some local variable store logic
}
In general, we will implement the success
and failure
callbacks, but in some scenarios we will ignore some callbacks. For example, when preloading data, we don't need to handle the failure
callback. After all, the preloading is called in the background, error message never response to UI and can be drop. The code we call is as follows:
sampleRequest(success: { (isSuccess, text) in
// do some complex thing
}, failure: {})
Here we used the empty Closure. In fact, it is simple enough, because the failure
callback carry no data. But if our preload is completed inside the method, then the success
callback also won't need to be implemented, the effect of the call will become like this:
sampleRequest(success: { _, _ in }, failure: {})
At this time, the empty Closure becomes less simple. We must declare _
according to the actual number of arguments to ignore the returned data. If the number of callback arguments changes in the future, we also need to manually maintain this empty Closure.
Some people may say that we can use the optional callback with the default argument nil
in sampleRequest()
to implement the effect of ignoring the callback, but we need to handle more optional content, and in some cases we cannot modify the implement code, like third party library.
Case 2: Use Closure to replace delegate
Just as UIKit has added block callbacks to replace simple delegates when we still use Objective-C. In Swift, because Closure is much easier to use than Objective-C blocks, we use Closure callbacks more often to achieve simple delegate effects. Such as I have three clickable buttons. Need to notify other objects, we can declare three Closure objects without using the delegate method:
Class A {
var didTapButtonA: () -> () = {}
var didTapButtonB: (Int, URL?) -> () = { _, _ in }
var didTapButtonC: (Int, Int, Int, Int, URL?) -> () = { _, _, _, _, _ in }
}
Then the corresponding Closure should be called when the button is triggered, and the object that needs to get the notification can implement this property like this:
let a = A()
a.didTapButtonA = {
// some logic code
}
However, we can see that didTapButtonB
and didTapButtonC
are quite troublesome. The problem is that we need to match the number of arguments to implement the empty Closure, and the subsequent updates also need to be updated synchronously. Similarly we can use optional type and replace the default value with nil
, but we should avoid using optional type as much as possible.
Simplified solution
First we create a struct Closure
:
public struct Closure {}
For ignoring callbacks, we add the following static methods:
static func ignore() {}
static func ignore(_: Any?) {}
static func ignore(_: Any?, _: Any?) {}
static func ignore(_: Any?, _: Any?, _: Any?) {}
static func ignore(_: Any?, _: Any?, _: Any?, _: Any?) {}
static func ignore(_: Any?, _: Any?, _: Any?, _: Any?, _: Any?) {}
static func ignore(_: Any?, _: Any?, _: Any?, _: Any?, _: Any?, _: Any?) {}
Then our two example calls can be simplified to the following effect, we don’t need to call back how many arguments, and others developer would be clear that we deliberately ignore these callbacks.
sampleRequest(success: { (isSuccess, text) in
// do some complex thing
}, failure: Closure.ignore)
sampleRequest(success: Closure.ignore, failure: Closure.ignore)
We can’t implement ignore
for all situations. But implement for up to 6 arguments can handle most of the situation, just like Swift's Tuple's Equatable implementation, which only implements up to 6 elements
When the Closure object is declared, ignore
is not suitable, so I choose empty
as the method name. At the same time we need to take advantage of a feature of func
, we can call with the same name but return different objects for different type.
static func empty() -> (() -> ()) { return {} }
static func empty() -> ((Any?) -> ()) { return { _ in } }
static func empty() -> ((Any?, Any?) -> ()) { return { _, _ in } }
static func empty() -> ((Any?, Any?, Any?) -> ()) { return { _, _, _ in } }
static func empty() -> ((Any?, Any?, Any?, Any?) -> ()) { return { _, _, _, _ in } }
static func empty() -> ((Any?, Any?, Any?, Any?, Any?) -> ()) { return { _, _, _, _, _ in } }
static func empty() -> ((Any?, Any?, Any?, Any?, Any?, Any?) -> ()) { return { _, _, _, _, _, _ in } }
Then the properties declare can be simplified to this effect:
class A {
var didTapButtonA: () -> () = Closure.empty()
var didTapButtonB: (Int, URL?) -> () = Closure.empty()
var didTapButtonC: (Int, Int, Int, Int, URL?) -> () = Closure.empty()
}
In this way, the use of empty Closure is completely unified and simplified, eliminating the cost of maintenance.
In fact, we can further simplify, remove the structure of struct Closure
and extract the Closure.ignore
and Closure.empty()
methods into global methods. But global method not recommended for Swift and also prone to interference local variables and methods. So it is recommended to retain the struct Closure
structure.
- All code in this article can be found in the GitHub project.
- If you have questions or suggestions, welcome to leave comment for discuss.
- If you feel this article is valuable, please forward it so more people can see it.
- If you like this type of content, welcome to follow my Medium and Twitter, I will keep posting useful content for everyone.