Do You Know Best Use Case Of Swift defer statement

Behaviour & Common Use Case With Examples

Ashok Rawat
6 min readSep 22, 2022
source

A defer statement is used for executing code just before transferring program control outside of the current block of code that the defer statement appears in. It means, defer statement is used to execute a piece of code exactly before the execution departs the current block of code. The current block of code might be a method, an if statement, a do block, a loop or a switch case.

The statements within the defer are executed no matter how program control is transferred, which makes it perfect for running clean-up code like closing files, closing connections to a database, manually allocated memory or releasing other system resources.

A defer statements block is :

defer {
// Statements
}

Use cases of defer statement

There are many situations in the project where we can use defer statements. Following are few of the real-world use cases in iOS swift development for perfect use of defer statement doing clean-up code like closing files, closing connections to a database, manually allocated memory or releasing other system resources.

1. Unlock Lock (NSLock)

The most common use case for the Swift defer statement is to unlock a lock. defer can ensure this state is updated even if the code has multiple paths. This removes any worry about forgetting to unlock, which could result in a memory leak or a deadlock.

var balance = 1000.0 // shared resource
let lock = NSLock()
struct Bank { func withdraw(amount: Double) {
lock.lock()
// defer will ensure lock is unlock before control is transferred
defer {
lock.unlock()
}
if balance > amount {
balance -= amount
print("balance is \(balance)")
} else {
print("insufficent balance")
}
}
}

2. Cleanup Manual Memory Allocation

If you access C APIs and create CoreFoundation objects, allocate memory, or read and write files with fopen, FileHandle. You can make sure to use defer statement to clean up properly in all cases with dealloc, free, close, deallocate, closeFile.

a) UnsafeMutablePointer:- UnsafeMutablePointer provides no automated memory management or alignment guarantees. You are responsible for handling the life cycle of any memory you work with through unsafe pointers to avoid leaks or undefined behaviour. Use defer block to deallocate it.

do {    // allocate memory    let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
pointer.initialize(repeating: 0, count: count)
// deallocate memory using defer defer {
pointer.deinitialize(count: count)
pointer.deallocate()
}
pointer.pointee = 4
pointer.advanced(by: 1).pointee = 2
let bufferPointer = UnsafeBufferPointer(start: pointer, count: count)
for bufPointer in bufferPointer.enumerated() {
print("value \(bufPointer)")
}
}

b) UIGraphicsEndImageContext:- If we are using to creates a bitmap-based graphics context using UIGraphicsBeginImageContextWithOptions with the specified options. By calling UIGraphicsEndImageContext inside the defer statement, we ensure the graphics context end before the exit.

private func drawRoundedImageWithCOrner() -> UIImage? { 
let bounds = CGRect(x: 0, y: 0, width:200, height: 200)
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)
defer {
UIGraphicsEndImageContext()
}
UIColor.black.setFill()
let path = UIBezierPath(roundedRect: bounds,
byRoundingCorners: UIRectCorner.allCorners,
cornerRadii: CGSize(width: 10, height: 10))
path.addClip()
UIRectFill(bounds)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}

3. Update View Layout

If we are updating the constraints programmatically, we can put layoutIfNeeded() inside the defer statement. This will enable us to update the constraints without any worry of forgetting to call layoutIfNeeded(). Similar way setNeedsLayout() method can be used inside defer to update the view which will ensure that the method is always executed before exiting the scope.

private func updateViewContstraints(_ blueView: UIView) {
defer {
self.view.layoutIfNeeded()
}
NSLayoutConstraint(item: blueView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: 80.0).isActive = true
}

4. Committing Changes

a) CATransaction:- The defer statement may be used to commit all changes made using CATransaction. This ensures that the animation transaction will always be committed even if there is a conditional code after the defer statement that returns early.

private func shakeAnimateView(_ redView: UIView){
CATransaction.begin()
defer {
CATransaction.commit()
}
let animate: CABasicAnimation = CABasicAnimation(keyPath: "position")
animate.duration = 0.1
animate.repeatCount = 21
animate.autoreverses = true
let from_point:CGPoint = CGPointMake(redView.center.x - 5, redView.center.y)
let from_value:NSValue = NSValue(cgPoint: from_point)
let to_point:CGPoint = CGPointMake(redView.center.x + 5, redView.center.y)
let to_value:NSValue = NSValue(cgPoint: to_point)
animate.fromValue = from_value
animate.toValue = to_value
redView.layer.add(animate, forKey: "position")
}

b) AVCaptureSession commitConfiguration:- By calling commitConfiguration() inside the defer statement, we ensure the AVCaptureSession configuration changes are committed before the exit. There may be many docatch statements result in an early exit when an error is thrown

private func setupSessionInput(for position: AVCaptureDevice.Position) {
var captureSession: AVCaptureSession = AVCaptureSession()
do {
guard let device = return AVCaptureDevice.default(AVCaptureDevice.DeviceType.builtInWideAngleCamera,for: AVMediaType.video, position: position) else {
}
let input = try AVCaptureDeviceInput(device: device)
try device.lockForConfiguration()
captureSession.beginConfiguration()
defer {
captureSession.commitConfiguration()
}
device.autoFocusRangeRestriction = .near
device.focusMode = .continuousAutoFocus
device.exposureMode = .continuousAutoExposure
if let currentInput = captureSession.inputs.filter({$0 is AVCaptureDeviceInput}).first {
captureSession.removeInput(currentInput)
}
captureSession.usesApplicationAudioSession = false
captureSession.addInput(input)
device.unlockForConfiguration()
} catch(let error) {
print(error.localizedDescription)
return
}
}

Difference Between Defer And Return

The defer statement is executed no matter how you exit and it works no matter you exit the current scope, which might not involve return . defer works for a function body, a while block, an if, a do block, and so on. There might be more than one return in your method, or you might throw an error, or have a break, or when you just reach the last line of the scope, the defer is executed in every possible case. The defer statement is executed after the return.

struct checkCounter: Sequence, IteratorProtocol { 
var count: Int
mutating func next() -> Int? {
if count == 0 {
return nil
}
else {
defer
{ count -= 1 }
return count
}
}
}

In above code next() function, first return count and then decrement it count value by 1.

Defer With Fallthrough

The fallthrough keyword doesn’t check the case conditions for the switch case that it causes execution to fall into. The fallthrough keyword simply causes code execution to move directly to the statements inside the next case (or default case) block, as in C’s standard switch statement behaviour. In the below code snippet case: block is the end of the scope of that block and fallthrough statement doesn't maintain the scope of a switch so case 2 will also execute.

defer { 
print("outer defer")
}
let counter = 1
switch counter {
case 0:
print("case 0")
case 1:
print("case 1")
defer { print("case 1 defer") }
fallthrough
case 2:
print("case 2")
default:
print("default")
}
// Output
case 1
case 1 defer
case 2

Using Multiple Defer Statements

If multiple defer statements appear in the same scope, each defer statement is actually executed in a first-in-last-out (FILO) order. That means, the last defer statement in the source code order executes first and the first defer statement executes last.

We can see few examples below to understand the multiple defer statements:

--------------------------------------------------------------------
Example 1
--------------------------------------------------------------------
func testDefer() {
defer { print("First defer") }
defer { print("Second defer") }
defer { print("Third defer") }
print("Do some work here")
}
// Output
Do some work here
Third defer
Second defer
First defer
--------------------------------------------------------------------
Example 2
--------------------------------------------------------------------
func multipleDefer() {
defer {
print("Defer 1")
defer {
print("defer 2")
}
defer {
print("Defer 3")
defer {
print("Defer 4")
defer {
print("Defer 5")
}
}
defer {
print("Defer 6")
}
}
}
}
// Output
Defer 1
Defer 3
Defer 6
Defer 4
Defer 5
defer 2

Limitations Of Defer

Any Swift statement can be included in the body of a defer statement but there is one exception. The defer statement can not contain any code that would transfer control out of the defer statement. This means that defer can’t include statements such as the break or return statements or throw any errors within the body of the defer statement as these statements would cause execution of that defer block to be immediately terminated.

let isDeferExecute: Bool?defer { 
guard let _ = isDeferExecute else {
// ERROR 'return' cannot transfer control out of a defer statement
return
}
print("check defer with return")

Thanks for reading, you can follow me on Medium for updated articles.

If you have any comments, questions, or recommendations feel free to post them in the comment section below! 👇 and please share and give claps 👏👏 if you liked this post.

--

--

Ashok Rawat

Mobile Engineer iOS | Swift | Objective-C | Python | React | Unity3D | Flutter