Verify that a completion block runs or is passed to another block using Swift

David Hoerl
Mac O’Clock
Published in
3 min readMay 17, 2020
Photo by Nine Köpfer on Unsplash

Introduction

This issue has bothered me for ages: I inherit some existing code that makes heavy use of completion blocks, and some methods run for several pages (don’t blame me! I didn’t write it!). Obviously I can vet the code at some point in time—but later on, a small change could result in a case where the block never runs or is run twice.

I just had to edit an extended Swift class where the primary class is ~1500 lines, and with a half dozen extension files nearly doubled the size. Virtually every function defines a trailing completion block! I knew I could carefully vet each method, but what I really wanted was a way to annotate the code so that not only was it correct now, but would stay correct when edited later.

💡😆🎉

During a long walk, the solution slowly dawned on me. Swift, offers a wide suite of tools to choose from, and I realized I could use defer and let x: Foo together. First, defer insures that the supplied block always runs when the enclosing context exits.

Secondly, Swift allows us define a property that will be set later on, while insuring that it cannot be used until set:

let x: Intif something {
x = 5
} else {
x = 1
}
print(x)

Aha! I could insure that x was set before the context exits by adding:

defer { let _ = x }

The Technique

The solution is actually pretty simple. In your method signature, modify the completion block parameter so that the label remains the same, but the actual variable has a prepended underscore:

func foo(<parameters>, completionBlock _completionBlock: SomeType){

Then, define the block without the underscore:

let completionBlock: SomeType

If you do this on existing code, Xcode will light it up with red lines in the right scroll bar, since every existing usage of that property is accessing an undefined property.

Now for the first bit of clang magic: add a defer statement referencing completionBlockimmediately after declaration:

let completionBlock: SomeType
defer { let _ = completionBlock }

Now, the compiler will not let you compile this method if any exit point has not set completionBlock.

Finally, we combine into a procedure:

  1. If the block is passed into another closure, then immediately above that you set completionBlock to _completionBlock.
  2. If the block will be executed, then set completionBlock as in 1, then execute it completionBlock(x).

This is now virtually fool proof. If code calls completionBlock, then falls through to some other place where completionBlock is set the code won’t compile, thus insuring the block isn’t called more than once. As long as the two procedures are adhered to, we know for sure that the block is either called once and only once, or its passed on! 🎉🎉🎉

Code Example

Note in the above code that in every new context, the same two lines appear:

let completionBlock: SomeType
defer { let _ = completionBlock }

While the above example is trivial for demonstration, I can assure you that the real code I was looking at had hundreds of lines and even deeper nesting!

Conclusion

You can employ the above technique to insure that complex existing code properly handles completion blocks without having to refactor a massive class, and it can be done incrementally over time.

This technique insures that completion blocks either run and run once, or are passed on. This technique will be relatively easy on PR reviewers, as the changes are surgical and minimal. A refactor might be the ideal approach, but may require significant review and testing.

This technique is not 100% fault proof: someone could call_completionBlock directly, or set completionBlock but not call or pass it. The first can be detected by adding an Xcode run script that searches for methods starting with an underscore that have an immediately following “(“ character.

The second is something that a PR diff would highlight, since a set must be followed by a call or pass (no need to scan back in the code looking for something).

It took me about four hours to convert over two dozen methods in a 3000 line class. Dozens of fails were detected, some I don’t believe I would have detected by reading the code alone.

Thanks for reading this far, and I hope this technique proves useful to you someday!

--

--