The Swift defer
statement allows you to execute code right before scope is exited (for example, before a function returns). This post presents an overview of Swift defer
and the most common examples for the defer
keyword:
Swift Defer Keyword
The multiple use cases for the Swift defer
keyword can be grouped into two main categories:
- Defer Completion. Like the Defer Completion example, a defer statement can be used ensure a completion block is called with appropriate values before a function returns.
- Defer Logic. Like the Defer Logic example, a defer statement can be used to ensure key logic like closing a file executes before a function returns.
Check out the Swift defer examples in this post to see how the defer
keyword can be used defer completion and defer logic.
Defer After Return
In Swift defer
will execute after return
. Thus, a defer
statement can ensure critical state is updated before a function returns even if your code has multiple branches and paths. For example, a lock can be unlocked using a defer
statement to ensure the lock is always unlocked before the function return
statements:
func criticalSectionWork(task: Task, lock: NSLock) -> Int {
// lock.unlock() will always be called before
// the return statement
defer {
lock.unlock()
}
// Handle results
lock.lock()
let result = task.perform()
if result == .success {
modifyDatabase(task: task)
return 0
}
else if result == .error {
return -1
}
else {
return -2
}
}
Defer Completion
Managing state with multiple branches and cases can lead to error prone code. A Swift defer
statement can be used to ensure a completion block is always called on completion with the correct variables regardless of the branches taken:
func process(tasks: [Tasks], completion: ([Int]?, Error?) -> Void) {
var results = [Int]()
var error: Error?
// completion will be always executed when this
// function ends, even if there are multiple branches
// of logic later in the function implementation
defer {
completion(results, error)
}
for task in tasks {
do {
results.append(task.perform())
}
catch let taskError {
error = taskError
break
}
}
if let error = error {
results = nil
}
}
Defer Logic
The defer
statement allows you to ensure a block of code is executed at the end of a function, for example, closing a file:
func write(data: Data, offset: Int) throws {
// Compute the filepath to write to
let documentsPath = NSSearchPathForDirectoriesInDomains(
.documentDirectory,
.userDomainMask,
true
)[0]
let filename = "data.txt"
let filepath = "\(documentsPath)/\(filename)"
// Open a file handle
guard let file = FileHandle(forUpdatingAtPath: filepath) else {
throw WriteError.notFound
}
// Ensure the file is closed at the end of this function
defer {
try? file.close()
}
// Seek to specified offset
try file.seek(toOffset: UInt64(offset))
// Write data at specified offset
file.write(data)
}
Defer Return Value
An incorrect use of a defer statement in Swift is to attempt to modify the return value. The following example demonstrates why using the defer
keyword to modify the return value is not a good solution:
func status() -> String {
var status = "initial"
// Modifying a value type in the defer statement
// will not modify the return value
defer {
// This assignment does not work
status = "active"
}
return status
}
// Calling the status function will
// return "initial"
status()
In some cases, a defer
statement is able to modify the return value. For example, logic executed in a defer
statement would modify a return value if the return value is a persistent object or reference.
In these cases it is still improper to use defer
to modify the return value. Modifying the return value in a defer
statement may make debugging difficult, particularly for complex functions. Additionally, if a return value is modified in a defer
statement there is often a more logical place for the modification to occur.
Swift Defer Statement
That’s it! By using defer
in Swift you can execute important logic after the return, defer logic, and defer completion callbacks to ensure completion blocks are called.