TinyExtension: Simplified empty Closure

Galvin Li
5 min readFeb 17, 2019

--

此文章同时提供中文版本: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.

--

--

Galvin Li

A Tiny iOS developer who love to solve problems and make things better.